#5478 Release 5.14.1
Merged a month ago by wombelix. Opened a month ago by wombelix.
wombelix/pagure feat_release_5.14.1  into  5.14.x

Support Redis Unix sockets
Georg Pfuetzenreuter • a month ago  
Fix package unretirement URL
Ewoud Kohl van Wijngaarden • a month ago  
file modified
+2 -2
@@ -20,7 +20,7 @@ 

  Playground version: https://stg.pagure.io

  

  If you have any questions or just would like to discuss about pagure,

- feel free to drop by on IRC in the channel ``#pagure`` of the freenode server

+ feel free to drop by `our Matrix room <https://matrix.to/#/#pagure:fedora.im>`_.

  

  

  About its name
@@ -59,7 +59,7 @@ 

  

      $ docker-compose -f dev/docker-compose.yml exec web python3 dev-data.py --all

  

- You can then login with any of the created users, by example:

+ You can then log in with any of the created users, by example:

  

  - username: pingou

  - password: testing123

file modified
+8
@@ -42,6 +42,14 @@ 

  

  * LOGGING_GIT_HOOKS

  

+ .. warning:: When upgrading MySQL database you can encounter issue with

+        incompatible columns. In this case try to check the collation of the

+        mentioned column by ``show full columns from <table>;`` in ``mysql``.

+        If the collation is something else then `utf8` you need to convert

+        your database to `utf8` first. See

+        `this guide <https://stackoverflow.com/questions/1294117/how-to-change-collation-of-database-table-column>`_

+        for how to do it or if you have SQL script for db setup,

+        you can change it directly in the script.

  

  From 5.9 to 5.10

  ----------------

@@ -19,6 +19,7 @@ 

      op.alter_column(

          'commit_flags',

          column_name='token_id',

+         existing_type=sa.String(64),

          nullable=False,

          existing_nullable=True

      )
@@ -28,6 +29,7 @@ 

      op.alter_column(

          'commit_flags',

          column_name='token_id',

+         existing_type=sa.String(64),

          nullable=True,

          existing_nullable=False

      )

@@ -6,7 +6,7 @@ 

  

  ### Set the time after which the admin session expires

  # There are two sessions on pagure, login that holds for 31 days and

- # the session defined here after which an user has to re-login.

+ # the session defined here after which an user has to log in again.

  # This session is used when accessing all administrative parts of pagure

  # (ie: changing a project's or a user's settings)

  ADMIN_SESSION_LIFETIME = timedelta(minutes=20000000)
@@ -20,7 +20,7 @@ 

  

  ### url to the database server:

  #DB_URL=mysql://user:pass@host/db_name

- #DB_URL=postgres://user:pass@host/db_name

+ #DB_URL=postgresql://user:pass@host/db_name

  DB_URL = 'sqlite:////srv/git/pagure_dev.sqlite'

  

  ### The FAS group in which the admin of pagure are

@@ -4,7 +4,7 @@ 

  Documentation=https://pagure.io/pagure

  

  [Service]

- ExecStart=celery -A pagure.lib.tasks worker --loglevel=info -c 1 -Q authorized_keys_queue

+ ExecStart=celery -A pagure.lib.tasks worker --loglevel=INFO -c 1 -Q authorized_keys_queue

  Environment="PAGURE_CONFIG=/etc/pagure/pagure.cfg"

  Type=simple

  Restart=on-failure

@@ -5,7 +5,7 @@ 

  

  [Service]

  Environment="PAGURE_CONFIG=/etc/pagure/pagure.cfg"

- ExecStart=celery -A pagure.lib.tasks worker --loglevel=info -Q pagure_ci

+ ExecStart=celery -A pagure.lib.tasks worker --loglevel=INFO -Q pagure_ci

  Type=simple

  User=git

  Group=git

@@ -5,7 +5,7 @@ 

  

  [Service]

  Environment="PAGURE_CONFIG=/etc/pagure/pagure.cfg"

- ExecStart=celery -A pagure.lib.tasks worker --loglevel=info -Q pagure_webhook

+ ExecStart=celery -A pagure.lib.tasks worker --loglevel=INFO -Q pagure_webhook

  Type=simple

  User=git

  Group=git

@@ -5,7 +5,7 @@ 

  

  [Service]

  Environment="PAGURE_CONFIG=/etc/pagure/pagure.cfg"

- ExecStart=celery -A pagure.lib.tasks worker --loglevel=info

+ ExecStart=celery -A pagure.lib.tasks worker --loglevel=INFO

  Type=simple

  User=git

  Group=git

@@ -21,7 +21,7 @@ 

      glibc-langpack-en

  

  RUN cd / \

-     && GIT_TRACE=1 GIT_CURL_VERBOSE=1 git clone -b $BRANCH $REPO \

+     && GIT_TRACE=1 git clone -b $BRANCH $REPO \

      && chmod +x /pagure/dev/containers/tox_py3.sh \

      && sed -i -e 's|"alembic-3"|"alembic"|' /pagure/tests/test_alembic.py

  

@@ -24,7 +24,7 @@ 

  RUN pip install pagure-messages

  

  RUN cd / \

-     && GIT_TRACE=1 GIT_CURL_VERBOSE=1 git clone -b $BRANCH $REPO \

+     && GIT_TRACE=1 git clone -b $BRANCH $REPO \

      && chmod +x /pagure/dev/containers/runtests_py3.sh

  

  # Install all the requirements from the spec file and replace the macro

file modified
+1 -1
@@ -6,7 +6,7 @@ 

  RUN mkdir /code

  WORKDIR /code

  

- ENTRYPOINT ["/usr/bin/celery-3", "-A", "pagure.lib.tasks_services", "worker", "--loglevel", "info", "-Q", "pagure_logcom"]

+ ENTRYPOINT ["/usr/bin/celery-3", "-A", "pagure.lib.tasks_services", "worker", "--loglevel", "INFO", "-Q", "pagure_logcom"]

  

  # Code injection is last to make optimal use of caches

  VOLUME ["/code"]

@@ -11,7 +11,7 @@ 

  git remote rm proposed || true

  git gc --auto

  git remote add proposed "$REPO"

- GIT_TRACE=1 GIT_CURL_VERBOSE=1 git fetch proposed

+ GIT_TRACE=1 git fetch proposed

  git checkout origin/master

  git config --global user.email "you@example.com"

  git config --global user.name "Your Name"
@@ -24,4 +24,4 @@ 

  

  sed -i -e "s|#!/usr/bin/env python|#!/usr/bin/env python3|" pagure/hooks/files/hookrunner

  

- pytest-3 -n auto ${TESTCASE:-tests/}

+ pytest-3 -rf -n auto ${TESTCASE:-tests/}

file modified
+1 -1
@@ -11,7 +11,7 @@ 

  git remote rm proposed || true

  git gc --auto

  git remote add proposed "$REPO"

- GIT_TRACE=1 GIT_CURL_VERBOSE=1 git fetch proposed

+ GIT_TRACE=1 git fetch proposed

  git checkout origin/master

  git config --global user.email "you@example.com"

  git config --global user.name "Your Name"

file modified
+1 -1
@@ -6,7 +6,7 @@ 

  RUN mkdir /code

  WORKDIR /code

  

- ENTRYPOINT ["/usr/bin/celery-3", "-A", "pagure.lib.tasks", "worker", "--loglevel", "info"]

+ ENTRYPOINT ["/usr/bin/celery-3", "-A", "pagure.lib.tasks", "worker", "--loglevel", "INFO"]

  

  # Code injection is last to make optimal use of caches

  VOLUME ["/code"]

file modified
+75
@@ -3,6 +3,81 @@ 

  

  This document records all notable changes to `Pagure <https://pagure.io>`_.

  

+ 5.14.1 (2024-05-24)

+ -------------------

+ 

+ Feature:

+ 

+ - Support Redis Unix sockets `#5437 <https://pagure.io/pagure/pull-request/5437>`_ (Georg Pfuetzenreuter)

+ - Add a new admin command to clean a spam user `#5392 <https://pagure.io/pagure/pull-request/5392>`_ (Ryan Lerch)

+ - Add config option to restrict creating by OIDC groups `#5399 <https://pagure.io/pagure/pull-request/5399>`_ (Ryan Lerch)

+ - Add api endpoints for adding/removing user to group `#5416 <https://pagure.io/pagure/pull-request/5416>`_ (Michal Konecny)

+ - Add new monitoring options for release monitoring `#5294 <https://pagure.io/pagure/pull-request/5294>`_ (Michal Konečný)

+ - Add API endpoint for reopening pull requests `#5291 <https://pagure.io/pagure/pull-request/5291>`_ (Matej Focko)

+ - Add info about changed files to push notification payload `#5435 <https://pagure.io/pagure/pull-request/5435>`_ (Nikola Forró)

+ - Enable pull_request_update acl for user tokens `#5436 <https://pagure.io/pagure/pull-request/5436>`_ (Maja Massarini)

+ - Enable collaborators to merge pull requests `#5438 <https://pagure.io/pagure/pull-request/5438>`_ (Samyak Jain)

+ - Add pull request ID to push notification payload `#5452 <https://pagure.io/pagure/pull-request/5452>`_ (Nikola Forró)

+ - Owner of a PR can update is own PR `#5457 <https://pagure.io/pagure/pull-request/5457>`_ (Maja Massarini)

+ - Add history button to the tree view `#5184 <https://pagure.io/pagure/pull-request/5184>`_ (Anatoli Babenia)

+ - dist-git: Added a condition that decides whether to use the stg or prod version of the link `#5468 <https://pagure.io/pagure/pull-request/5468>`_ (amedvede)

+ 

+ Fix:

+ 

+ - Reduce noise, remove GIT_CURL_VERBOSE=1 from git commands `#5440 <https://pagure.io/pagure/pull-request/5440>`_ (Frank Dana)

+ - libravatar.org account login url `#5367 <https://pagure.io/pagure/pull-request/5367>`_ (Dominik Wombacher)

+ - wrong reply icon in issues and pull requests `#5361 <https://pagure.io/pagure/pull-request/5361>`_ (Dominik Wombacher)

+ - Reorder celery arguments (and s/info/INFO/) `#5215 <https://pagure.io/pagure/pull-request/5215>`_ (Sergio Durigan Junior)

+ - Please make sure you give each node a unique nodename using the celery worker '-n' option `#5332 <https://pagure.io/pagure/pull-request/5332>`_ (Sérgio M. Basto)

+ - TypeError: BaseEventLoop.create_server() got an unexpected keyword argument 'loop' `#5332 <https://pagure.io/pagure/pull-request/5332>`_ (Sérgio M. Basto)

+ - dialect 'postgres://' deprecated in sqlalchemy `#5357 <https://pagure.io/pagure/pull-request/5357>`_ (Dominik Wombacher)

+ - Ensure the url we redirect to are full URLs `#5355 <https://pagure.io/pagure/pull-request/5355>`_ (Pierre-Yves Chibon)

+ - BROKER_URL default that honors REDIS_PORT and REDIS_DB `#5358 <https://pagure.io/pagure/pull-request/5358>`_ (Dominik Wombacher)

+ - Fix grammar issue `#5241 <https://pagure.io/pagure/pull-request/5241>`_ (AJ Jordan)

+ - Update Translation Status button `#5246 <https://pagure.io/pagure/pull-request/5246>`_ (Sundeep Anand)

+ - Crash when config:[ENABLE_DOCS = False] `#5475 <https://pagure.io/pagure/pull-request/5475>`_ (Dominik Wombacher)

+ - Added data-toggle attribute to missing tooltips `#5474 <https://pagure.io/pagure/pull-request/5474>`_ (Dominik Wombacher)

+ - Drop the ssh key from the information stored in the cookie `#5249 <https://pagure.io/pagure/pull-request/5249>`_ (Pierre-Yves Chibon)

+ - Fix for mysql alembic migration `#5280 <https://pagure.io/pagure/pull-request/5280>`_ (Michal Konečný)

+ - PR close in API for user token `#5206 <https://pagure.io/pagure/pull-request/5206>`_ (Michal Konečný)

+ - Ensuring integer for mqtt port, needed by paho-mqtt `#5290 <https://pagure.io/pagure/pull-request/5290>`_ (Fabian Arrotin)

+ - Change 'Browse All' emoji link `#5295 <https://pagure.io/pagure/pull-request/5295>`_ (Benjamin A. Beasley)

+ - Fix package unretirement URL `#5296 <https://pagure.io/pagure/pull-request/5296>`_ (Ewoud Kohl van Wijngaarden)

+ - error when logging exception on event listener `#5319 <https://pagure.io/pagure/pull-request/5319>`_ (Michal Konečný)

+ - typo on pagure_authorized_keys_worker.service `#5331 <https://pagure.io/pagure/pull-request/5331>`_ (Sérgio M. Basto)

+ - fix object of 'rebased onto' comment `#5341 <https://pagure.io/pagure/pull-request/5341>`_ (Adam Williamson)

+ - mirror_project_in: unused '--check' arg removed, description adjusted `#5356 <https://pagure.io/pagure/pull-request/5356>`_ (Dominik Wombacher)

+ - theme: long words in source nav break layout `#5363 <https://pagure.io/pagure/pull-request/5363>`_ (Dominik Wombacher)

+ - user_settings: unable to change default email `#5364 <https://pagure.io/pagure/pull-request/5364>`_ (Dominik Wombacher)

+ - cannot import name 'escape' from 'jinja2' `#5360 <https://pagure.io/pagure/pull-request/5360>`_ (Dominik Wombacher)

+ - Fix query filter for date ranges `#5385 <https://pagure.io/pagure/pull-request/5385>`_ (Michal Konečný)

+ - cli/admin: Shorten message to fit length rule for the style check `#5398 <https://pagure.io/pagure/pull-request/5398>`_ (Neal Gompa)

+ - grammar of "log in" `#5382 <https://pagure.io/pagure/pull-request/5382>`_ (Zbigniew Jędrzejewski-Szmek)

+ - English improvements `#5454 <https://pagure.io/pagure/pull-request/5454>`_ (Jerry James)

+ - Update pagure/themes/srcfpo/templates/repo_info.html `#5446 <https://pagure.io/pagure/pull-request/5446>`_ (Sundeep Anand)

+ - Changed logic with using PDC for getting inactive branches to use bodhi instead `#5419 <https://pagure.io/pagure/pull-request/5419>`_ (amedvede)

+ - File history page breadcrumb fixes `#5201 <https://pagure.io/pagure/pull-request/5201>`_ (L. Guruprasad)

+ - Extra whitespace in "packages" on src.fpo front page `#5476 <https://pagure.io/pagure/pull-request/5476>`_ (Dominik Wombacher)

+ 

+ Security Fix:

+ 

+ - Argument Injection in PagureRepo.log() rhbz#2277121 `#5481 <https://pagure.io/pagure/pull-request/5481>`_ (Thomas Chauchefoin)

+ - CVE-2024-4982: Path traversal in view_issue_raw_file() rhbz#2279411 `#5484 <https://pagure.io/pagure/pull-request/5484>`_ (Thomas Chauchefoin and Dominik Wombacher)

+ - CVE-2024-4981: _update_file_in_git() follows symbolic links in temporary clones rhbz#2278745 `#5483 <https://pagure.io/pagure/pull-request/5483>`_ (Thomas Chauchefoin and Dominik Wombacher)

+ - generate_archive() follows symbolic links in temporary clones rhbz#2280030 `#5482 <https://pagure.io/pagure/pull-request/5482>`_ (Thomas Chauchefoin and Dominik Wombacher)

+ 

+ Docs:

+ 

+ - Add some basic documentation for boards `#5237 <https://pagure.io/pagure/pull-request/5237>`_ (Ben Cotton)

+ - Document MySQL migration issue `#5282 <https://pagure.io/pagure/pull-request/5282>`_ (Michal Konečný)

+ - Add API key information to Jenkins CI setup `#5347 <https://pagure.io/pagure/pull-request/5347>`_ (Michal Konečný)

+ - Add Global security step to Jenkins CI configuration `#5348 <https://pagure.io/pagure/pull-request/5348>`_ (Michal Konečný)

+ - Update pagure CI guide `#5349 <https://pagure.io/pagure/pull-request/5349>`_ (Michal Konečný)

+ - install: filename apache sample config wrong `#5362 <https://pagure.io/pagure/pull-request/5362>`_ (Dominik Wombacher)

+ - Update chatroom reference to the new official Matrix room `#5409 <https://pagure.io/pagure/pull-request/5409>`_ (Neal Gompa)

+ - Remove deprecated smart_strong xtn `#5430 <https://pagure.io/pagure/pull-request/5430>`_ (Frank Dana)

+ 

+ 

  5.13.3 (2021-11-01)

  -------------------

  - Warn users when a PR contains some characters

file modified
+25 -6
@@ -44,7 +44,7 @@ 

  ::

  

      DB_URL = 'mysql://user:pass@host/db_name'

-     DB_URL = 'postgres://user:pass@host/db_name'

+     DB_URL = 'postgresql://user:pass@host/db_name'

      DB_URL = 'sqlite:////var/tmp/pagure_dev.sqlite'

  

  Defaults to ``sqlite:////var/tmp/pagure_dev.sqlite``
@@ -105,10 +105,10 @@ 

  is the broker that is used to communicate between the web application and

  its workers.

  

- Defaults to: ``'redis://%s' % APP.config['REDIS_HOST']``

+ Defaults to: ``"redis://%s:%d/%d" % (pagure_config["REDIS_HOST"], pagure_config["REDIS_PORT"], pagure_config["REDIS_DB"])``

  

- .. note:: See the :ref:`redis-section` for the ``REDIS_HOST`` configuration

-           key

+ .. note:: See the :ref:`redis-section` for the ``REDIS_HOST``, ``REDIS_PORT``

+           and ``REDIS_DB``configuration keys

  

  

  Repo Directories
@@ -1266,6 +1266,25 @@ 

  Defaults to: ``True``

  

  

+ RESTRICT_CREATE_BY_OIDC_GROUP

+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

+ 

+ This configuration key, when defined, only allows users that are a member of the group defined

+ the ability to create new projects and groups.

+ 

+ Defaults to: ``None``

+ 

+ 

+ RESTRICT_CREATE_BY_OIDC_GROUP_COUNT

+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

+ 

+ This configuration key, when defined, only allows users that are a member of the group defined

+ by RESTRICT_CREATE_BY_OIDC_GROUP and a member of at least the number of groups defined by this

+ key the ability to create new projects.

+ 

+ Defaults to: 0

+ 

+ 

  ENABLE_DEL_PROJECTS

  ~~~~~~~~~~~~~~~~~~~

  
@@ -1747,9 +1766,9 @@ 

  ~~~~~~~~~~~~~~~~~~~~~~

  

  This configuration key allows specifying the lifetime of the session during

- which the user won't have to re-login for admin actions.

+ which the user won't have to log in again for admin actions.

  In other words, the maximum time between which an user can access a project's

- settings page without re-login.

+ settings page without a re-login.

  

  Defaults to: ``timedelta(minutes=20)``

  

file modified
+2 -2
@@ -29,5 +29,5 @@ 

     idea but don't know how to implement it, you just have something bugging

     you?

  

-    Come to see us on IRC: ``#pagure`` or ``#fedora-apps`` on

-    irc.freenode.net or directly on `the project <http://pagure.io>`_.

+    Come to see us on Matrix: ``#pagure:fedora.im`` or directly on

+    `the project <http://pagure.io>`_.

file modified
+30 -11
@@ -3,13 +3,13 @@ 

  

  Pagure would be nothing without its contributors.

  

- On November 1, 2021 (release 5.13.3) the list looks as follow:

+ On May 24, 2024 (release 5.14.1) the list looks as follow:

  

  =================  ===========

  Number of commits  Contributor

  =================  ===========

-   6901              Pierre-Yves Chibon <pingou@pingoured.fr>

-    328              Ryan Lerch <rlerch@redhat.com>

+   6905              Pierre-Yves Chibon <pingou@pingoured.fr>

+    330              Ryan Lerch <rlerch@redhat.com>

     172              Vivek Anand <vivekanand1101@gmail.com>

     147              Julen Landa Alustiza <jlanda@fedoraproject.org>

     139              farhaanbukhsh <farhaan.bukhsh@gmail.com>
@@ -33,8 +33,10 @@ 

      19              Gaurav Kumar <aavrug@gmail.com>

      19              Lenka Segura <lenka@sepu.cz>

      18              Abhijeet Kasurde <akasurde@redhat.com>

+     18              Adam Williamson <awilliam@redhat.com>

+     18              Dominik Wombacher <dominik@wombacher.cc>

+     18              Michal Konečný <mkonecny@redhat.com>

      18              Sayan Chowdhury <sayan.chowdhury2012@gmail.com>

-     17              Adam Williamson <awilliam@redhat.com>

      17              Brian Stinson <brian@bstinson.com>

      17              Ralph Bean <rbean@redhat.com>

      15              Igor Gnatenko <ignatenkobrain@fedoraproject.org>
@@ -43,6 +45,7 @@ 

      13              Ghost-script <subho.prp@gmail.com>

      13              Martin Basti <mbasti@redhat.com>

      13              Mathieu Bridon <bochecha@fedoraproject.org>

+     11              Anatoli Babenia <anatoli@rainforce.org>

      11              Shengjing Zhu <zsj950618@gmail.com>

       9              Björn Persson <Bjorn@Rombobjörn.se>

       9              Michael Watters <michael.watters@dart.biz>
@@ -50,13 +53,12 @@ 

       8              Lei Yang <yltt1234512@gmail.com>

       8              Michael Scherer <misc@redhat.com>

       8              Paul W. Frields <stickster@gmail.com>

-      7              Anatoli Babenia <anatoli@rainforce.org>

+      8              Sergio Durigan Junior <sergiodj@sergiodj.net>

       7              René Genz <liebundartig@freenet.de>

-      7              Sergio Durigan Junior <sergiodj@sergiodj.net>

       7              zPlus <zplus@peers.community>

-      6              Michal Konečný <mkonecny@redhat.com>

       6              ymdatta <ymdatta@protonmail.com>

       5              Fabio Valentini <decathorpe@gmail.com>

+      5              FeRD (Frank Dana) <ferdnyc@gmail.com>

       5              Lukas Holecek <hluk@email.cz>

       5              Mike McLean <mikem@redhat.com>

       5              Oliver Gutierrez <ogutierrez@redhat.com>
@@ -70,6 +72,7 @@ 

       4              Eric Barbour <ebarbour@redhat.com>

       4              Maciej Lasyk <maciek@lasyk.info>

       4              Miro Hrončok <miro@hroncok.cz>

+      4              Nikola Forró <nforro@redhat.com>

       4              clime <clime@redhat.com>

       3              Akanksha <akanksha_mishra01@yahoo.com>

       3              Ankush Behl <cloudbehl@gmail.com>
@@ -77,15 +80,18 @@ 

       3              Chenxiong Qi <cqi@redhat.com>

       3              Dhriti Shikhar <dhriti.shikhar.rokz@gmail.com>

       3              Eric Barbour <emb4gu@virginia.edu>

-      3              FeRD (Frank Dana) <ferdnyc@gmail.com>

       3              Jan Pokorný <jpokorny@redhat.com>

       3              Jason Tibbitts <tibbs@math.uh.edu>

       3              Kushal Khandelwal <kushal124@gmail.com>

+      3              L. Guruprasad <lgp171188@gmail.com>

+      3              Maja Massarini <mmassari@redhat.com>

       3              Pedro Lima <pedro.lima@gmail.com>

       3              Pierre-YvesChibon <pingou@fedoraproject.org>

       3              Ricky Elrod <ricky@elrod.me>

       3              Ryan Lerch <rlerch@localhost.localdomain>

       3              Stefan Bühler <stbuehler@web.de>

+      3              Sérgio M. Basto <sergio@serjux.com>

+      3              amedvede <amedvede@redhat.com>

       3              bill auger <mr.j.spam.me@gmail.com>

       3              cep <breathingcode@gmail.com>

       3              shivani <smshivani579@gmail.com>
@@ -98,10 +104,12 @@ 

       2              Fabian Arrotin <fabian.arrotin@arrfab.net>

       2              František Zatloukal <fzatlouk@redhat.com>

       2              Hervé Beraud <hberaud@redhat.com>

+      2              Jerry James <loganjerry@gmail.com>

       2              Kamil Páral <kparal@redhat.com>

       2              Luis Guzman <ark@switnet.org>

       2              MR <mrx@mailinator.com>

       2              Mohan Boddu <mboddu@bhujji.com>

+      2              Neal Gompa <neal@gompa.dev>

       2              Neha Kandpal <iec2015048@iiita.ac.in>

       2              Nuno Maltez <nuno@cognitiva.com>

       2              Ompragash <om.apsara@gmail.com>
@@ -110,17 +118,20 @@ 

       2              Richard Marko <rmarko@fedoraproject.org>

       2              Simo Sorce <simo@redhat.com>

       2              Stasiek Michalski <hellcp@opensuse.org>

+      2              Sundeep Anand <suanand@redhat.com>

       2              Tim Flink <tflink@fedoraproject.org>

       2              Tim Landscheidt <tim@tim-landscheidt.de>

       2              Todd Zullinger <tmz@pobox.com>

       2              William Moreno Reyes <williamjmorenor@gmail.com>

       2              Your Name <jlanda@fedoraproject.org>

+      2              Zbigniew Jędrzejewski-Szmek <zbyszek@in.waw.pl>

       2              bruno <bruno@wolff.to>

       2              dhrish20 <dhrish20@gmail.com>

       2              hellcp <hellcp@opensuse.org>

       2              siddharthvipul <siddharthvipul1@gmail.com>

       2              yadneshk <yadnesh45@gmail.com>

       2              “AnjaliPardeshi” <“anjalipardeshi92@gmail.com”>

+      1              AJ Jordan <alex@strugee.net>

       1              Akanksha Mishra <akanksha_mishra01@yahoo.com>

       1              Aleksandra Fedorova (bookwar) <afedorova@mirantis.com>

       1              Alexander Scheel <ascheel@redhat.com>
@@ -130,6 +141,8 @@ 

       1              Anthony Lackey <alackey@localhost.localdomain>

       1              Antoni Segura Puimedon <celebdor@gmail.com>

       1              Arti Laddha <artiladdha53@gmail.com>

+      1              Ben Cotton <bcotton@fedoraproject.org>

+      1              Benjamin A. Beasley <code@musicinmybrain.net>

       1              Brendan Early <mymindstorm@evermiss.net>

       1              Brian (bex) Exelbierd <bex@pobox.com>

       1              Carl George <carl@george.computer>
@@ -138,14 +151,16 @@ 

       1              David Caro <dcaroest@redhat.com>

       1              Devesh Kumar Singh <deveshkusingh@gmail.com>

       1              Eashan <eashankadam@gmail.com>

+      1              Ewoud Kohl van Wijngaarden <ewoud@kohlvanwijngaarden.nl>

+      1              Fabian Arrotin <arrfab@centos.org>

       1              Felix Yan <felixonmars@users.sf.net>

       1              Filip Valder <fvalder@redhat.com>

       1              Frank Dana (FeRD) <ferdnyc@gmail.com>

+      1              Georg Pfuetzenreuter <mail@georg-pfuetzenreuter.net>

       1              Haikel Guemar <hguemar@fedoraproject.org>

       1              Hazel Smith <hazel@hazelesque.uk>

       1              Jan Kuparinen <copper_fin@hotmail.com>

       1              Jeremy Cline <jcline@redhat.com>

-      1              Jerry James <loganjerry@gmail.com>

       1              Jingjing Shao <sanri.ok@163.com>

       1              John Florian <jflorian@doubledog.org>

       1              Jun Aruga <jaruga@redhat.com>
@@ -157,7 +172,9 @@ 

       1              Lukas Brabec <lbrabec@redhat.com>

       1              Mark O Brien <markobri@redhat.com>

       1              Mary Kate Fain <mk@marykatefain.com>

+      1              Matej Focko <mfocko@redhat.com>

       1              Mathew Robinson <mathew.robinson3114@gmail.com>

+      1              Michal Konecny <mkonecny@redhat.com>

       1              Michal Srb <michal@redhat.com>

       1              Michel Alexandre Salim <michel@michel-slm.name>

       1              Mohan Boddu <mboddu@redhat.com>
@@ -171,17 +188,18 @@ 

       1              Romain DEP. <rom1dep@gmail.com>

       1              Ryan Lerch <ryanlerch@gmail.com>

       1              Sachin Kamath <sskamath96@gmail.com>

+      1              Samyak Jain <samyak.jn11@gmail.com>

       1              Snehal Karale <skarale@redhat.com>

       1              Stanislav Laznicka <slaznick@redhat.com>

       1              Stanislav Ochotnicky <sochotnicky@redhat.com>

       1              Stephen Gallagher <sgallagh@redhat.com>

-      1              Sundeep Anand <suanand@redhat.com>

+      1              Sundeep Anand <suanand@fedoraproject.org>

+      1              Thomas Chauchefoin <thomas@chauchefoin.fr>

       1              Tiago M. Vieira <tiago@tvieira.com>

       1              Till Hofmann <hofmann@kbsg.rwth-aachen.de>

       1              Vadim Rutkovsky <vrutkovs@redhat.com>

       1              Vyacheslav Anzhiganov <vanzhiganov@ya.ru>

       1              Yves Martin <ymartin1040@gmail.com>

-      1              Zbigniew Jędrzejewski-Szmek <zbyszek@in.waw.pl>

       1              abhishek <abhishekarora12@gmail.com>

       1              abhishek goswami <abhishekg785@gmail.com>

       1              alunux <fadlun.net@gmail.com>
@@ -191,6 +209,7 @@ 

       1              anshukira <aks.anshu03@gmail.com>

       1              chocos10 <iec2015048@iiita.ac.in>

       1              d3prof3t <saurabhpysharma@gmail.com>

+      1              four_4 <fruitloopsgo@gmail.com>

       1              ishcherb <ishcherb@redhat.com>

       1              jcvicelli <jcvicelli@gmail.com>

       1              josef radinger <cheese@nosuchhost.net>

file modified
+14 -14
@@ -101,19 +101,19 @@ 

  

  # Install the additional files as follow:

  

- +------------------------------+------------------------------------------+

- |         Source               |             Destination                  |

- +=============================+===========================================+

- | ``files/pagure.cfg.sample``  | ``/etc/pagure/pagure.cfg``               |

- +------------------------------+------------------------------------------+

- | ``files/alembic.ini``        | ``/etc/pagure/alembic.ini``              |

- +------------------------------+------------------------------------------+

- | ``files/pagure.conf``        | ``/etc/httpd/conf.d/pagure.conf``        |

- +------------------------------+------------------------------------------+

- | ``files/pagure.wsgi``        | ``/usr/share/pagure/pagure.wsgi``        |

- +------------------------------+------------------------------------------+

- | ``createdb.py``              | ``/usr/share/pagure/pagure_createdb.py`` |

- +------------------------------+------------------------------------------+

+ +------------------------------------+------------------------------------------+

+ |         Source                     |             Destination                  |

+ +====================================+==========================================+

+ | ``files/pagure.cfg.sample``        | ``/etc/pagure/pagure.cfg``               |

+ +------------------------------------+------------------------------------------+

+ | ``files/alembic.ini``              | ``/etc/pagure/alembic.ini``              |

+ +------------------------------------+------------------------------------------+

+ | ``files/pagure-apache-httpd.conf`` | ``/etc/httpd/conf.d/pagure.conf``        |

+ +------------------------------------+------------------------------------------+

+ | ``files/pagure.wsgi``              | ``/usr/share/pagure/pagure.wsgi``        |

+ +------------------------------------+------------------------------------------+

+ | ``createdb.py``                    | ``/usr/share/pagure/pagure_createdb.py`` |

+ +------------------------------------+------------------------------------------+

  

  

  
@@ -160,7 +160,7 @@ 

  at: ``/etc/httpd/conf.d/pagure.conf``.

  

  If not installed by RPM, the example file is present in the sources at:

- ``files/pagure.conf``.

+ ``files/pagure-apache-httpd.conf``.

  

  Adjust it for your needs.

  

file added
+44
@@ -0,0 +1,44 @@ 

+ Using Boards

+ ============

+ 

+ Pagure provides basic `kanban board <https://en.wikipedia.org/wiki/Kanban_(development)>`_ functionality.

+ This allows the state of issues to be represented visually.

+ The feature requires a specific, admin-defined tag to appear on a board.

+ A repository may contain multiple boards, each with a different tag.

+ 

+ 

+ Creating a Board

+ ----------------

+ 

+ #. From the ``Settings`` tab, select ``Boards``

+ #. Click the ``Add a new board`` button

+ #. Enter a descriptive name in the ``Board name`` text box

+ #. Select the tag to use in the ``Tag`` drop down

+ #. Ensure the ``Active`` checkbox is checked

+ #. Click the ``Update`` button to create the board

+ 

+ After the board is created, add the status columns.

+ 

+ #. While still on the ``Boards`` settings, click the wrench icon button

+ #. If you want to use the default statuses (``Backlog``, ``Triaged``, ``In Progress``, ``In Review``, ``Done``, ``Blocked``), click the ``Populate with defaults`` button.

+ #. If you wish to add non-default statuses, click the ``Add new status`` button

+     #. Enter a name for the status in the ``Status name`` text box

+     #. If you want this status to be the default for issues added to the board, select the ``Default`` radio button.

+     #. If you want this status to close the issue, check the ``Close`` check box

+     #. Select the ``Color`` for the status on the board. This is for visual distinctness; you do not have to change it.

+     #. Repeat until all of the desired statuses are added

+ #. Click and drag the arrows to reorder the statuses, if desired.

+ #. Click the ``Update`` button when finished.

+ 

+ Using Boards

+ ------------

+ 

+ To add an issue to a board, add the board's label to the issue.

+ Alternatively, you can add an existing issue to the board by clicking the plus sign on the desired status column and adding the issue number.

+ 

+ To change the status of an issue. go to the ``Boards`` tab and drag the card on the board into the desired status column.

+ The status appears on the issue under the ``Boards`` information, but it cannot be changed from the issue.

+ 

+ If you drag an issue to a column that has the ``Close`` boolean set, Pagure will automatically close the issue.

+ 

+ .. note:: If you close an issue directly, Pagure will remove the board's label.

file modified
+4 -4
@@ -6,8 +6,8 @@ 

  When coming to pagure for the first time there are a few things one should

  do or check to ensure all works as desired.

  

- Login to pagure or create your account

- --------------------------------------

+ Log in to pagure or create your account

+ ---------------------------------------

  

  Pagure has its own user account system.

  
@@ -16,7 +16,7 @@ 

  pagure.io, the Fedora Account System) via OpenID, the local user account

  is created upon login.

  

- This means, you cannot be added to a group or a project before you login for

+ This means, you cannot be added to a group or a project before you log in for

  the first time as the system will simply not know you.

  

  If you run your own pagure instance which uses the local authentication
@@ -54,7 +54,7 @@ 

  

  To upload your public key onto pagure:

  

- 1. Login into pagure and click on the user icon on

+ 1. Log in to pagure and click on the user icon on

  the top right corner, there, select ``My settings``.

  

  .. image:: _static/pagure_my_settings.png

file modified
+1
@@ -59,6 +59,7 @@ 

     upgrade_db

     pagure_ci

     quick_replies

+    board

     troubleshooting

     tips_tricks

  

file modified
-1
@@ -16,7 +16,6 @@ 

  - `Definition Lists <https://python-markdown.github.io/extensions/definition_lists/>`_

  - `Fenced Code Blocks <https://python-markdown.github.io/extensions/fenced_code_blocks/>`_

  - `Tables <https://python-markdown.github.io/extensions/tables/>`_

- - `Smart Strong <https://python-markdown.github.io/extensions/smart_strong/>`_

  - `Admonition <https://python-markdown.github.io/extensions/admonition/>`_

  - `CodeHilite <https://python-markdown.github.io/extensions/code_hilite/>`_

  - `Sane lists <https://python-markdown.github.io/extensions/sane_lists/>`_

@@ -121,3 +121,32 @@ 

      fi

  

      # Part of the script specific to how you run the tests on your project

+ 

+ * To use the URL to POST results you need to add CI token to Jenkins instance.

+   This token could be found in Pagure project `Settings` -> `Hooks` -> `Pagure CI`.

+   In Jenkins add it in `Manage Jenkins` -> `Manage Credentials` -> `Stores scoped to Jenkins`

+   -> `Jenkins` -> `Global credentials (unrestricted)` -> `Add Credentials` as kind

+   `Secret text` (ID will be used in script).

+ 

+ Example function used in Jenkins pipeline script

+ 

+ ::

+ 

+    # 'pagure-auth' is the ID of the credentials

+ 

+    def notifyPagurePR(repo, msg, status, phase, credentials = 'pagure-auth'){

+        def json = JsonOutput.toJson([name: 'pagure', url: env.JOB_NAME, build: [full_url: currentBuild.absoluteUrl, status: status, number: currentBuild.number, phase: phase]])

+        println json

+ 

+        withCredentials([string(credentialsId: credentials, variable: "PAGURE_PUSH_SECRET")]) {

+            /* We need to notify pagure that jenkins finished but then pagure will

+              wait for jenkins to be done, so if we wait for pagure's answer we're

+              basically stuck in a loop where both jenkins and pagure are waiting

+              for each other */

+            sh "timeout 1 curl -X POST -d \'$json\' https://pagure.io/api/0/ci/jenkins/$repo/\${PAGURE_PUSH_SECRET}/build-finished -H \"Content-Type: application/json\" | true"

+        }

+    }

+ 

+ * To be able to trigger builds from Pagure CI you need to change the Global Security. Go

+   to `Manage Jenkins` -> `Configure Global Security` and find `Authorization` section.

+   In `Matrix-based security` add Read permission to `Anonymous Users` for Overall/Job/View.

@@ -30,6 +30,13 @@ 

  above is disabled in favor of the `merge` option.

  

  

+ `Boards`

+ --------------------------

+ 

+ The boards feature provides simple kanban board functionality by showing issues in columns that represent state.

+ The settings page lists existing boards and allows adminisrators to add new boards.

+ 

+ 

  `Comment editing`

  --------------------------

  

file modified
+4 -14
@@ -3,9 +3,6 @@ 

  from __future__ import print_function, absolute_import

  import os

  import argparse

- from datetime import datetime, timedelta

- 

- from sqlalchemy.exc import SQLAlchemyError

  

  import pagure.config

  import pagure.lib.model as model
@@ -22,13 +19,13 @@ 

  _config = pagure.config.reload_config()

  

  

- def main(check=False, debug=False):

-     """ The function pulls in all the changes from upstream"""

+ def main(debug=False):

+     """The function pulls in all the changes from upstream"""

  

      session = pagure.lib.model_base.create_session(_config["DB_URL"])

      projects = (

          session.query(model.Project)

-         .filter(model.Project.mirrored_from != None)

+         .filter(model.Project.mirrored_from is not None)

          .all()

      )

  
@@ -47,14 +44,7 @@ 

  

  if __name__ == "__main__":

      parser = argparse.ArgumentParser(

-         description="Script to send email before the api token expires"

-     )

-     parser.add_argument(

-         "--check",

-         dest="check",

-         action="store_true",

-         default=False,

-         help="Print the some output but does not send any email",

+         description="Script to PULL external repos into local (mirroring)"

      )

      parser.add_argument(

          "--debug",

file modified
+2 -2
@@ -3,7 +3,7 @@ 

  

  ### Set the time after which the admin session expires

  # There are two sessions on pagure, login that holds for 31 days and

- # the session defined here after which an user has to re-login.

+ # the session defined here after which an user has to log in again.

  # This session is used when accessing all administrative parts of pagure

  # (ie: changing a project's or a user's settings)

  ADMIN_SESSION_LIFETIME = timedelta(minutes=20)
@@ -20,7 +20,7 @@ 

  

  ### url to the database server:

  #DB_URL = 'mysql://user:pass@host/db_name'

- #DB_URL = 'postgres://user:pass@host/db_name'

+ #DB_URL = 'postgresql://user:pass@host/db_name'

  DB_URL = 'sqlite:////var/tmp/pagure_dev.sqlite'

  

  ### Send FedMsg notifications of events in pagure

file modified
+4 -1
@@ -16,7 +16,7 @@ 

  

  

  Name:               pagure

- Version:            5.13.3

+ Version:            5.14.1

  Release:            1%{?dist}

  Summary:            A git-centered forge

  
@@ -568,6 +568,9 @@ 

  

  

  %changelog

+ * Fri May 24 2024 Dominik Wombacher <dominik@wombacher.cc> - 5.14.1-1

+ - Update to 5.14.1

+ 

  * Mon Nov 01 2021 Pierre-Yves Chibon <pingou@pingoured.fr> - 5.13.3-1

  - Update to 5.13.3

  

@@ -4,7 +4,7 @@ 

  Documentation=https://pagure.io/pagure

  

  [Service]

- ExecStart=/usr/bin/celery worker -A pagure.lib.tasks --loglevel=info -c 1 -Q authorized_keys_queue

+ ExecStart=/usr/bin/celery -A pagure.lib.tasks worker --loglevel=INFO -c 1 -Q authorized_keys_queue -n authorized_keys

  Environment="PAGURE_CONFIG=/etc/pagure/pagure.cfg"

  Type=simple

  User=git

file modified
+1 -1
@@ -8,7 +8,7 @@ 

  Documentation=https://pagure.io/pagure

  

  [Service]

- ExecStart=/usr/bin/celery worker -A pagure.lib.tasks_services --loglevel=info -Q pagure_ci

+ ExecStart=/usr/bin/celery -A pagure.lib.tasks_services worker --loglevel=INFO -Q pagure_ci

  Environment="PAGURE_CONFIG=/etc/pagure/pagure.cfg"

  Type=simple

  User=git

@@ -4,7 +4,7 @@ 

  Documentation=https://pagure.io/pagure

  

  [Service]

- ExecStart=/usr/bin/celery worker -A pagure.lib.tasks --loglevel=info -c 1 -Q gitolite_queue

+ ExecStart=/usr/bin/celery -A pagure.lib.tasks worker --loglevel=INFO -c 1 -Q gitolite_queue -n gitolite

  Environment="PAGURE_CONFIG=/etc/pagure/pagure.cfg"

  Type=simple

  User=git

@@ -8,7 +8,7 @@ 

  Documentation=https://pagure.io/pagure

  

  [Service]

- ExecStart=/usr/bin/celery worker -A pagure.lib.tasks_services --loglevel=info -Q pagure_loadjson

+ ExecStart=/usr/bin/celery -A pagure.lib.tasks_services worker --loglevel=INFO -Q pagure_loadjson -n load_json

  Environment="PAGURE_CONFIG=/etc/pagure/pagure.cfg"

  Type=simple

  User=git

file modified
+1 -1
@@ -8,7 +8,7 @@ 

  Documentation=https://pagure.io/pagure

  

  [Service]

- ExecStart=/usr/bin/celery worker -A pagure.lib.tasks_services --loglevel=info -Q pagure_logcom

+ ExecStart=/usr/bin/celery -A pagure.lib.tasks_services worker --loglevel=INFO -Q pagure_logcom -n logcom

  Environment="PAGURE_CONFIG=/etc/pagure/pagure.cfg"

  Type=simple

  User=git

file modified
+1 -1
@@ -13,7 +13,7 @@ 

  Documentation=https://pagure.io/pagure

  

  [Service]

- ExecStart=/usr/bin/celery worker -A pagure.lib.tasks_mirror --loglevel=info -Q pagure_mirror

+ ExecStart=/usr/bin/celery -A pagure.lib.tasks_mirror worker --loglevel=INFO -Q pagure_mirror

  Environment="PAGURE_CONFIG=/etc/pagure/pagure.cfg"

  Type=simple

  User=mirror

file modified
+1 -1
@@ -8,7 +8,7 @@ 

  Documentation=https://pagure.io/pagure

  

  [Service]

- ExecStart=/usr/bin/celery worker -A pagure.lib.tasks_services --loglevel=info -Q pagure_webhook

+ ExecStart=/usr/bin/celery -A pagure.lib.tasks_services worker --loglevel=INFO -Q pagure_webhook -n webhook

  Environment="PAGURE_CONFIG=/etc/pagure/pagure.cfg"

  Type=simple

  User=git

file modified
+1 -1
@@ -4,7 +4,7 @@ 

  Documentation=https://pagure.io/pagure

  

  [Service]

- ExecStart=/usr/bin/celery worker -A pagure.lib.tasks --loglevel=info

+ ExecStart=/usr/bin/celery -A pagure.lib.tasks worker --loglevel=INFO -n worker

  Environment="PAGURE_CONFIG=/etc/pagure/pagure.cfg"

  Type=simple

  User=git

@@ -10,7 +10,7 @@ 

  Documentation=https://pagure.io/pagure

  

  [Service]

- ExecStart=/usr/bin/celery worker -A pagure.lib.tasks --loglevel=info -Q <queue_name>

+ ExecStart=/usr/bin/celery -A pagure.lib.tasks worker --loglevel=INFO -Q <queue_name>

  Environment="PAGURE_CONFIG=/etc/pagure/pagure.cfg"

  Type=simple

  User=git

@@ -170,7 +170,7 @@ 

      try:

          obj = get_obj_from_path(url.path)

      except PagureException as err:

-         log.warning(err.message)

+         log.warning(str(err))

          return

  

      origin = pagure.config.config.get("APP_URL")
@@ -253,7 +253,6 @@ 

              handle_client,

              host=None,

              port=pagure.config.config["EVENTSOURCE_PORT"],

-             loop=loop,

          )

          SERVER = loop.run_until_complete(coro)

          log.info(
@@ -264,7 +263,6 @@ 

                  stats,

                  host=None,

                  port=pagure.config.config.get("EV_STATS_PORT"),

-                 loop=loop,

              )

              stats_server = loop.run_until_complete(stats_coro)

              log.info(

file modified
+1 -1
@@ -12,4 +12,4 @@ 

  

  

  __api_version__ = "0.31"

- __version__ = "5.13.3"

+ __version__ = "5.14.1"

file modified
+14 -1
@@ -20,6 +20,7 @@ 

  import functools

  import logging

  import os

+ import sys

  

  import docutils

  import enum
@@ -44,6 +45,9 @@ 

  

  _log = logging.getLogger(__name__)

  

+ # Mitigate bug in flask-wtf 0.14.2 on EL8 for 5.14.1 release

+ sys.modules["werkzeug.url_encode"] = "werkzeug.urls.url_encode"

+ 

  

  def preload_docs(endpoint):

      """ Utility to load an RST file and turn it into fancy HTML. """
@@ -113,6 +117,9 @@ 

      ENEWPROJECTDISABLED = (

          "Creating project have been disabled for this instance"

      )

+     ENEWPROJECTFORBIDDEN = (

+         "You are not allowed to create new projects on this instance"

+     )

      ETIMESTAMP = "Invalid timestamp format"

      EDATETIME = "Invalid datetime format"

      EINVALIDISSUEFIELD = "Invalid custom field submitted"
@@ -589,6 +596,7 @@ 

          fork.api_pull_request_merge,

          fork.api_pull_request_rebase,

          fork.api_pull_request_close,

+         fork.api_pull_request_reopen,

          fork.api_pull_request_add_comment,

          fork.api_pull_request_add_flag,

          fork.api_pull_request_get_flag,
@@ -609,7 +617,12 @@ 

      ]

      sections.append(build_docs_section("users", users_methods))

  

-     groups_methods = [group.api_groups, group.api_view_group]

+     groups_methods = [

+         group.api_groups,

+         group.api_view_group,

+         group.api_group_add_member,

+         group.api_group_remove_member,

+     ]

      sections.append(build_docs_section("groups", groups_methods))

  

      plugins_methods = [

file modified
+74 -3
@@ -503,7 +503,7 @@ 

      _check_token(repo, project_token=False)

  

      request = _get_request(repo, requestid)

-     _check_pull_request_access(request, assignee=True)

+     _check_pull_request_access(request, assignee=True, allow_author=True)

  

      form = pagure.forms.RequestPullForm(csrf_enabled=False)

      if not form.validate_on_submit():
@@ -770,10 +770,13 @@ 

  

      repo = _get_repo(repo, username, namespace)

      _check_pull_request(repo)

-     _check_token(repo)

+     _check_token(repo, project_token=False)

      request = _get_request(repo, requestid)

  

-     if not is_repo_committer(repo):

+     if (

+         not is_repo_committer(repo)

+         and not flask.g.fas_user.username == request.user.username

+     ):

          raise pagure.exceptions.APIError(403, error_code=APIERROR.ENOPRCLOSE)

  

      try:
@@ -791,6 +794,74 @@ 

      return jsonout

  

  

+ @API.route("/<repo>/pull-request/<int:requestid>/reopen", methods=["POST"])

+ @API.route(

+     "/<namespace>/<repo>/pull-request/<int:requestid>/reopen", methods=["POST"]

+ )

+ @API.route(

+     "/fork/<username>/<repo>/pull-request/<int:requestid>/reopen",

+     methods=["POST"],

+ )

+ @API.route(

+     "/fork/<username>/<namespace>/<repo>/pull-request/<int:requestid>/reopen",

+     methods=["POST"],

+ )

+ @api_login_required(acls=["pull_request_close", "pull_request_update"])

+ @api_method

+ def api_pull_request_reopen(repo, requestid, username=None, namespace=None):

+     """

+     Reopen a pull-request

+     --------------------

+     Instruct Pagure to reopen a pull request.

+ 

+     ::

+ 

+         POST /api/0/<repo>/pull-request/<request id>/reopen

+         POST /api/0/<namespace>/<repo>/pull-request/<request id>/reopen

+ 

+     ::

+ 

+         POST /api/0/fork/<username>/<repo>/pull-request/<request id>/reopen

+         POST /api/0/fork/<username>/<namespace>/<repo>/pull-request/<request id>/reopen

+ 

+     Sample response

+     ^^^^^^^^^^^^^^^

+ 

+     ::

+ 

+         {

+           "message": "Pull-request reopened!"

+         }

+ 

+     """  # noqa

+     output = {}

+ 

+     repo = _get_repo(repo, username, namespace)

+     _check_pull_request(repo)

+     _check_token(repo, project_token=False)

+     request = _get_request(repo, requestid)

+ 

+     if (

+         not is_repo_committer(repo)

+         and not flask.g.fas_user.username == request.user.username

+     ):

+         raise pagure.exceptions.APIError(403, error_code=APIERROR.ENOPRCLOSE)

+ 

+     try:

+         pagure.lib.query.reopen_pull_request(

+             flask.g.session, request, flask.g.fas_user.username

+         )

+         flask.g.session.commit()

+         output["message"] = "Pull-request reopened!"

+     except SQLAlchemyError as err:  # pragma: no cover

+         flask.g.session.rollback()

+         _log.exception(err)

+         raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR)

+ 

+     jsonout = flask.jsonify(output)

+     return jsonout

+ 

+ 

  @API.route("/<repo>/pull-request/<int:requestid>/comment", methods=["POST"])

  @API.route(

      "/<namespace>/<repo>/pull-request/<int:requestid>/comment",

file modified
+179
@@ -12,6 +12,7 @@ 

  from __future__ import unicode_literals, absolute_import

  

  import flask

+ from sqlalchemy.exc import SQLAlchemyError

  

  import pagure

  import pagure.exceptions
@@ -19,6 +20,8 @@ 

  from pagure.api import (

      API,

      APIERROR,

+     api_login_optional,

+     api_login_required,

      api_method,

      api_login_optional,

      get_page,
@@ -289,3 +292,179 @@ 

      jsonout = flask.jsonify(output)

      jsonout.status_code = 200

      return jsonout

+ 

+ 

+ @API.route("/group/<group>/add", methods=["POST"])

+ @api_login_required(acls=["group_modify"])

+ @api_method

+ def api_group_add_member(group):

+     """

+     Add member to group

+     -------------------

+     Add new member to group. To be able to add users to group the requester

+     needs to have permissions to do that.

+ 

+     ::

+ 

+         POST /api/0/group/<group>/add

+ 

+     Input

+     ^^^^^

+ 

+     +---------------------+--------+-------------+-----------------------------+

+     | Key                 | Type   | Optionality | Description                 |

+     +=====================+========+=============+=============================+

+     | ``user``            | string | Mandatory   | | User to add as member     |

+     |                     |        |             |   of group                  |

+     +---------------------+--------+-------------+-----------------------------+

+ 

+     Sample response

+     ^^^^^^^^^^^^^^^

+ 

+     ::

+ 

+         {

+           "creator": {

+             "default_email": "user1@example.com",

+             "emails": [

+               "user1@example.com"

+             ],

+             "fullname": "User1",

+             "name": "user1"

+           },

+           "date_created": "1492011511",

+           "description": "Some Group",

+           "display_name": "Some Group",

+           "group_type": "user",

+           "members": [

+             "user1",

+             "user2"

+           ],

+           "name": "some_group_name"

+         }

+ 

+     """  # noqa

+ 

+     group = pagure.lib.query.search_groups(flask.g.session, group_name=group)

+     if not group:

+         raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOGROUP)

+ 

+     # Validate inputs

+     form = pagure.forms.AddUserToGroupForm(meta={"csrf": False})

+     if not form.validate_on_submit():

+         raise pagure.exceptions.APIError(

+             400, error_code=APIERROR.EINVALIDREQ, errors=form.errors

+         )

+     else:

+         # Add user to group

+         try:

+             pagure.lib.query.add_user_to_group(

+                 flask.g.session,

+                 username=form.user.data,

+                 group=group,

+                 user=flask.g.fas_user.username,

+                 is_admin=pagure.utils.is_admin(),

+             )

+             flask.g.session.commit()

+             pagure.lib.git.generate_gitolite_acls(

+                 project=None, group=group.group_name

+             )

+         except (pagure.exceptions.PagureException, SQLAlchemyError) as err:

+             flask.g.session.rollback()

+             raise pagure.exceptions.APIError(

+                 400, error_code=APIERROR.EDBERROR, errors=[str(err)]

+             )

+ 

+     # Return the updated group

+     output = group.to_json(public=(not pagure.utils.api_authenticated()))

+     jsonout = flask.jsonify(output)

+     jsonout.status_code = 200

+     return jsonout

+ 

+ 

+ @API.route("/group/<group>/remove", methods=["POST"])

+ @api_login_required(acls=["group_modify"])

+ @api_method

+ def api_group_remove_member(group):

+     """

+     Remove member from group

+     ------------------------

+     Remove member from group. To be able to remove users from group the requester

+     needs to have permissions to do that.

+ 

+     ::

+ 

+         POST /api/0/group/<group>/remove

+ 

+     Input

+     ^^^^^

+ 

+     +---------------------+--------+-------------+-----------------------------+

+     | Key                 | Type   | Optionality | Description                 |

+     +=====================+========+=============+=============================+

+     | ``user``            | string | Mandatory   | | User to add as member     |

+     |                     |        |             |   of group                  |

+     +---------------------+--------+-------------+-----------------------------+

+ 

+     Sample response

+     ^^^^^^^^^^^^^^^

+ 

+     ::

+ 

+         {

+           "creator": {

+             "default_email": "user1@example.com",

+             "emails": [

+               "user1@example.com"

+             ],

+             "fullname": "User1",

+             "name": "user1"

+           },

+           "date_created": "1492011511",

+           "description": "Some Group",

+           "display_name": "Some Group",

+           "group_type": "user",

+           "members": [

+             "user1",

+             "user2"

+           ],

+           "name": "some_group_name"

+         }

+ 

+     """  # noqa

+ 

+     group = pagure.lib.query.search_groups(flask.g.session, group_name=group)

+     if not group:

+         raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOGROUP)

+ 

+     # Validate inputs

+     form = pagure.forms.AddUserToGroupForm(meta={"csrf": False})

+     if not form.validate_on_submit():

+         raise pagure.exceptions.APIError(

+             400, error_code=APIERROR.EINVALIDREQ, errors=form.errors

+         )

+     else:

+         # Remove user to group

+         try:

+             pagure.lib.query.delete_user_of_group(

+                 flask.g.session,

+                 username=form.user.data,

+                 groupname=group.group_name,

+                 user=flask.g.fas_user.username,

+                 is_admin=pagure.utils.is_admin(),

+             )

+             flask.g.session.commit()

+             pagure.lib.git.generate_gitolite_acls(

+                 project=None, group=group.group_name

+             )

+         except (pagure.exceptions.PagureException, SQLAlchemyError) as err:

+             flask.g.session.rollback()

+             raise pagure.exceptions.APIError(

+                 400, error_code=APIERROR.EDBERROR, errors=[str(err)]

+             )

+ 

+     # Return the updated group

+     output = group.to_json(public=(not pagure.utils.api_authenticated()))

+     jsonout = flask.jsonify(output)

+     jsonout.status_code = 200

+     return jsonout

file modified
+5
@@ -1453,6 +1453,11 @@ 

              404, error_code=APIERROR.ENEWPROJECTDISABLED

          )

  

+     if pagure_config["PAGURE_AUTH"] == 'oidc' and flask.g.fas_user.can_create is False:

+         raise pagure.exceptions.APIError(

+             403, error_code=APIERROR.ENEWPROJECTFORBIDDEN

+         )

+ 

      namespaces = pagure_config["ALLOWED_PREFIX"][:]

      if user:

          namespaces.extend([grp for grp in user.groups])

file modified
+7 -3
@@ -194,12 +194,14 @@ 

          )

  

  

- def _check_pull_request_access(request, assignee=False):

+ def _check_pull_request_access(request, assignee=False, allow_author=False):

      """Check if user can access Pull-Request. Must be repo committer

-     or author to see private pull-requests.

+     or author (if flag is true) to see private pull-requests.

      :param request: PullRequest object

      :param assignee: a boolean specifying whether to allow the assignee or not

          defaults to False

+     :param allow_author: a boolean specifying whether the PR author should be

+         allowed, defaults to False

      :raises pagure.exceptions.APIError: when access denied

      """

      # Private PRs require commit access
@@ -207,7 +209,9 @@ 

  

      error = False

      # Public tickets require ticket access

-     error = not is_repo_user(request.project)

+     error = not is_repo_user(request.project) and not (

+         allow_author and request.user.user == flask.g.fas_user.username

+     )

  

      if assignee:

          if (

file modified
+175
@@ -440,6 +440,30 @@ 

      local_parser.set_defaults(func=do_delete_project)

  

  

+ def _parser_sanitize_spam_user(subparser):

+     """Set up the CLI argument parser for the delete-user-activity action.

+ 

+     :arg subparser: an argparse subparser allowing to have action's specific

+         arguments

+ 

+     """

+     local_parser = subparser.add_parser(

+         "sanitize-spam-user",

+         help="Delete repos and tickets by the user specified",

+     )

+ 

+     local_parser.add_argument(

+         "user",

+         help="Username of the spam user to sanitize",

+     )

+     local_parser.add_argument(

+         "action_user",

+         help="Username of the admin user doing the action (ie: deleting the "

+         "users activity)",

+     )

+     local_parser.set_defaults(func=do_sanitize_spam_user)

+ 

+ 

  def _parser_create_branch(subparser):

      """Set up the CLI argument parser for the create-branch action.

  
@@ -579,6 +603,9 @@ 

      # delete-project

      _parser_delete_project(subparser)

  

+     # delete-all-user-acitvity

+     _parser_sanitize_spam_user(subparser)

+ 

      # create-branch

      _parser_create_branch(subparser)

  
@@ -925,6 +952,154 @@ 

      print("Project deleted")

  

  

+ def do_sanitize_spam_user(args):

+     """Block and remove activity by a spam user

+ 

+     :arg args: the argparse object returned by ``parse_arguments()``.

+ 

+     """

+     _log.debug("user:          %s", args.user)

+     _log.debug("user deleting: %s", args.action_user)

+ 

+     # Validate users

+     user = pagure.lib.query.get_user(session, args.user)

+     action_user = pagure.lib.query.get_user(session, args.action_user)

+ 

+     projects = (

+         session.query(pagure.lib.model.Project).filter(

+             pagure.lib.model.Project.user_id == user.id

+         )

+     ).all()

+ 

+     issues = (

+         session.query(pagure.lib.model.Issue).filter(

+             pagure.lib.model.Issue.user_id == user.id

+         )

+     ).all()

+ 

+     comments = (

+         session.query(pagure.lib.model.IssueComment).filter(

+             pagure.lib.model.IssueComment.user_id == user.id

+         )

+     ).all()

+ 

+     prs = (

+         session.query(pagure.lib.model.PullRequest).filter(

+             pagure.lib.model.PullRequest.user_id == user.id

+         )

+     ).all()

+ 

+     prcomments = (

+         session.query(pagure.lib.model.PullRequestComment).filter(

+             pagure.lib.model.PullRequestComment.user_id == user.id

+         )

+     ).all()

+ 

+     groups = (

+         session.query(pagure.lib.model.PagureGroup).filter(

+             pagure.lib.model.PagureGroup.user_id == user.id

+         )

+     ).all()

+ 

+     blocked = bool(

+         pagure.lib.query.get_blocked_users(

+             session, username=user.user, date=None

+         )

+     )

+ 

+     print("# Projects")

+     if projects:

+         for project in projects:

+             print(f"* {project.full_url}")

+     else:

+         print("    ***User has no Projects***")

+ 

+     print("# Issues")

+     if issues:

+         for issue in issues:

+             print(f"* {issue.full_url}")

+     else:

+         print("    ***User has no Issues***")

+ 

+     print("# Blocked Status")

+     if blocked:

+         print("    ***User is already blocked***")

+     else:

+         print("    User is NOT blocked on pagure")

+ 

+     if issues or projects or not blocked:

+         print("\nAre you sure you want to:")

+         if projects:

+             print(f"* DELETE {len(projects)} projects")

+         if issues:

+             print(f"* DELETE {len(issues)} issues")

+         if not blocked:

+             print(f"* BLOCK {user.user} on pagure until 2099-01-01")

+ 

+         if not _ask_confirmation():

+             return

+ 

+         if issues:

+             for issue in issues:

+                 print(f"DELETING ISSUE: {issue.full_url}")

+                 pagure.lib.query.drop_issue(

+                     session,

+                     issue=issue,

+                     user=action_user.user,

+                 )

+                 session.commit()

+ 

+         if projects:

+             for project in projects:

+                 print(f"DELETING PROJECT: {project.full_url}")

+                 pagure.lib.tasks.delete_project(

+                     namespace=project.namespace,

+                     name=project.name,

+                     user=project.user.user if project.is_fork else None,

+                     action_user=action_user.user,

+                 )

+                 session.commit()

+ 

+         if not blocked:

+             # block the user

+             print(f"BLOCKING USER: {user.user} until 2099-01-01")

+             date = arrow.get("2099-01-01", "YYYY-MM-DD").replace(tzinfo="UTC")

+             user.refuse_sessions_before = date.datetime

+             session.add(user)

+             session.commit()

+ 

+     print(f"\n\n# Activity by {user.user} that needs to be removed manually:")

+ 

+     print("## Issue Comments")

+     if comments:

+         for comment in comments:

+             print(f"* {comment.issue.full_url}#comment-{comment.id}")

+     else:

+         print("    ***User has no Issue Comments***")

+     print("## Pull Requests")

+     if prs:

+         for pr in prs:

+             print(f"* {pr.full_url}")

+     else:

+         print("    ***User has no Pull Requests***")

+ 

+     print("## Pull Request Comments")

+     if prcomments:

+         for prcomment in prcomments:

+             print(

+                 f"* {prcomment.pull_request.full_url}#comment-{prcomment.id}"

+             )

+     else:

+         print("    ***User has no Pull Request Comments***")

+ 

+     print("## Groups Created by the User")

+     if groups:

+         for group in groups:

+             print(f"* {group.full_url}")

+     else:

+         print("    ***User has not created any Groups***")

+ 

+ 

  def do_update_acls(args):

      """Update the ACLs in the database from the list present in the

      configuration file.

file modified
+4
@@ -32,6 +32,10 @@ 

      # from config and we add them here.

      if config["ENABLE_DOCS"]:

          config["DOCS_FOLDER"] = os.path.join(config["GIT_FOLDER"], "docs")

+     else:

+         config[

+             "DOCS_FOLDER"

+         ] = None  # Avoid 'KeyError' Exception down the line

      if config["ENABLE_TICKETS"]:

          config["TICKETS_FOLDER"] = os.path.join(

              config["GIT_FOLDER"], "tickets"

file modified
+11
@@ -63,6 +63,13 @@ 

  # Enables / Disables creating projects on this pagure instance

  ENABLE_NEW_PROJECTS = True

  

+ # When using OIDC auth, users must be in this OIDC group to create new projects

+ RESTRICT_CREATE_BY_OIDC_GROUP = None

+ 

+ # When using OIDC auth, users must be a member of RESTRICT_NEW_PROJECTS_BY_OIDC_GROUP and in total

+ # this many groups to create new projects

+ RESTRICT_CREATE_BY_OIDC_GROUP_COUNT = 0

+ 

  # Enables / Disables deleting projects on this pagure instance

  ENABLE_DEL_PROJECTS = True

  
@@ -361,6 +368,7 @@ 

      "modify_git_alias": "Modify git aliases (create or delete)",

      "create_git_alias": "Create git aliases",

      "delete_git_alias": "Delete git aliases",

+     "group_modify": "Add/Remove members from group",

  }

  

  # List of ACLs which a regular user is allowed to associate to an API token
@@ -377,8 +385,10 @@ 

      "create_project",

      "fork_project",

      "modify_project",

+     "group_modify",

      "update_watch_status",

      "pull_request_create",

+     "pull_request_update",

      "commit",

  ]

  
@@ -392,6 +402,7 @@ 

      "pull_request_comment",

      "pull_request_merge",

      "generate_acls_project",

+     "group_modify",

      "commit_flag",

      "create_branch",

      "tag_project",

file modified
+1 -1
@@ -1,7 +1,7 @@ 

  Authentication

  ~~~~~~~~~~~~~~

  

- To access some endpoints, you need to login to Pagure using API token. You

+ To access some endpoints, you need to log in to Pagure using an API token. You

  can generate one in the project setting page.

  

  When sending HTTP request, include an ``Authorization`` field in the header

file modified
+10 -5
@@ -17,6 +17,7 @@ 

  import time

  import os

  import warnings

+ from six.moves.urllib.parse import urljoin

  

  import flask

  import pygit2
@@ -40,7 +41,6 @@ 

  else:

      perfrepo = None

  

- 

  logger = logging.getLogger(__name__)

  

  REDIS = None
@@ -50,8 +50,9 @@ 

      or pagure_config.get("PAGURE_CI_SERVICES")

  ):

      pagure.lib.query.set_redis(

-         host=pagure_config["REDIS_HOST"],

-         port=pagure_config["REDIS_PORT"],

+         host=pagure_config.get("REDIS_HOST", None),

+         port=pagure_config.get("REDIS_PORT", None),

+         socket=pagure_config.get("REDIS_SOCKET", None),

          dbname=pagure_config["REDIS_DB"],

      )

  
@@ -444,7 +445,9 @@ 

      return_point = flask.url_for("ui_ns.index")

      if "next" in flask.request.args:

          if pagure.utils.is_safe_url(flask.request.args["next"]):

-             return_point = flask.request.args["next"]

+             return_point = urljoin(

+                 flask.request.host_url, flask.request.args["next"]

+             )

  

      authenticated = pagure.utils.authenticated()

      auth = pagure_config.get("PAGURE_AUTH", None)
@@ -508,7 +511,9 @@ 

      return_point = flask.url_for("ui_ns.index")

      if "next" in flask.request.args:

          if pagure.utils.is_safe_url(flask.request.args["next"]):

-             return_point = flask.request.args["next"]

+             return_point = urljoin(

+                 flask.request.host_url, flask.request.args["next"]

+             )

  

      if not pagure.utils.authenticated():

          return flask.redirect(return_point)

file modified
+24 -4
@@ -62,7 +62,15 @@ 

  

      @classmethod

      def runhook(

-         cls, session, username, hooktype, project, repotype, repodir, changes

+         cls,

+         session,

+         username,

+         hooktype,

+         project,

+         repotype,

+         repodir,

+         changes,

+         pull_request,

      ):

          """Run a specific hook on a project.

  
@@ -81,6 +89,8 @@ 

              changes (dict): A dict with keys being the ref to update, values

                  being a tuple of (from, to).

                  For example: {'refs/heads/master': (hash_from, hash_to), ...}

+             pull_request (model.PullRequest or None): The pull request whose

+                 merge is initiating this hook run.

          """

          if hooktype == "pre-receive":

              cls.pre_receive(
@@ -90,6 +100,7 @@ 

                  repotype=repotype,

                  repodir=repodir,

                  changes=changes,

+                 pull_request=pull_request,

              )

          elif hooktype == "update":

              cls.update(
@@ -99,6 +110,7 @@ 

                  repotype=repotype,

                  repodir=repodir,

                  changes=changes,

+                 pull_request=pull_request,

              )

  

          elif hooktype == "post-receive":
@@ -109,12 +121,15 @@ 

                  repotype=repotype,

                  repodir=repodir,

                  changes=changes,

+                 pull_request=pull_request,

              )

          else:

              raise ValueError('Invalid hook type "%s"' % hooktype)

  

      @staticmethod

-     def pre_receive(session, username, project, repotype, repodir, changes):

+     def pre_receive(

+         session, username, project, repotype, repodir, changes, pull_request

+     ):

          """Run the pre-receive tasks of a hook.

  

          For args, see BaseRunner.runhook.
@@ -122,7 +137,9 @@ 

          pass

  

      @staticmethod

-     def update(session, username, project, repotype, repodir, changes):

+     def update(

+         session, username, project, repotype, repodir, changes, pull_request

+     ):

          """Run the update tasks of a hook.

  

          For args, see BaseRunner.runhook.
@@ -131,7 +148,9 @@ 

          pass

  

      @staticmethod

-     def post_receive(session, username, project, repotype, repodir, changes):

+     def post_receive(

+         session, username, project, repotype, repodir, changes, pull_request

+     ):

          """Run the post-receive tasks of a hook.

  

          For args, see BaseRunner.runhook.
@@ -390,6 +409,7 @@ 

                      repotype=repotype,

                      repodir=repodir,

                      changes=changes,

+                     pull_request=pull_request,

                  )

              except Exception as e:

                  if hooktype != "pre-receive" or debug:

file modified
+13 -2
@@ -164,7 +164,7 @@ 

  

  

  def send_notifications(

-     session, project, repodir, user, refname, revs, forced, oldrev

+     session, project, repodir, user, refname, revs, forced, oldrev, pr_id

  ):

      """Send out-going notifications about the commits that have just been

      pushed.
@@ -190,6 +190,12 @@ 

          authors.append(author)

  

      if revs:

+         changed_files = pagure.lib.git.get_changed_files(

+             revs[-1],

+             oldrev,

+             repodir,

+         )

+ 

          revs.reverse()

          print("* Publishing information for %i commits" % len(revs))

  
@@ -202,10 +208,12 @@ 

              branch=refname,

              forced=forced,

              authors=list(authors),

+             changed_files=changed_files,

              agent=user,

              repo=project.to_json(public=True)

              if not isinstance(project, six.string_types)

              else project,

+             pull_request_id=pr_id,

          )

  

          # Send blink notification to any 3rd party plugins, if there are any
@@ -307,7 +315,9 @@ 

      """ Runner for the default hook."""

  

      @staticmethod

-     def post_receive(session, username, project, repotype, repodir, changes):

+     def post_receive(

+         session, username, project, repotype, repodir, changes, pull_request

+     ):

          """Run the default post-receive hook.

  

          For args, see BaseRunner.runhook.
@@ -436,6 +446,7 @@ 

                  commits,

                  forced,

                  oldrev,

+                 pull_request.id if pull_request else None,

              )

  

              # Now display to the user if this isn't the default branch links to

file modified
+3 -1
@@ -69,7 +69,9 @@ 

  

  class MailRunner(BaseRunner):

      @staticmethod

-     def post_receive(session, username, project, repotype, repodir, changes):

+     def post_receive(

+         session, username, project, repotype, repodir, changes, pull_request

+     ):

          """Run the multimail post-receive hook.

  

          For args, see BaseRunner.runhook.

file modified
+3 -1
@@ -68,7 +68,9 @@ 

      """ Runner for the mirror hook. """

  

      @staticmethod

-     def post_receive(session, username, project, repotype, repodir, changes):

+     def post_receive(

+         session, username, project, repotype, repodir, changes, pull_request

+     ):

          """Run the default post-receive hook.

  

          For args, see BaseRunner.runhook.

@@ -63,7 +63,9 @@ 

      """ Runner for the hook blocking force push. """

  

      @staticmethod

-     def pre_receive(session, username, project, repotype, repodir, changes):

+     def pre_receive(

+         session, username, project, repotype, repodir, changes, pull_request

+     ):

          """Run the pre-receive tasks of a hook.

  

          For args, see BaseRunner.runhook.

file modified
+3 -1
@@ -215,7 +215,9 @@ 

      """ Runner for the pagure's specific git hook. """

  

      @staticmethod

-     def post_receive(session, username, project, repotype, repodir, changes):

+     def post_receive(

+         session, username, project, repotype, repodir, changes, pull_request

+     ):

          """Run the default post-receive hook.

  

          For args, see BaseRunner.runhook.

@@ -59,7 +59,9 @@ 

      """ Runner for the hook blocking new branches from being created. """

  

      @staticmethod

-     def pre_receive(session, username, project, repotype, repodir, changes):

+     def pre_receive(

+         session, username, project, repotype, repodir, changes, pull_request

+     ):

          """Run the pre-receive tasks of a hook.

  

          For args, see BaseRunner.runhook.

@@ -64,7 +64,9 @@ 

      """

  

      @staticmethod

-     def post_receive(session, username, project, repotype, repodir, changes):

+     def post_receive(

+         session, username, project, repotype, repodir, changes, pull_request

+     ):

          """Run the default post-receive hook.

  

          For args, see BaseRunner.runhook.

@@ -65,7 +65,9 @@ 

      """ Runner for the git hook updating the DB of tickets on push. """

  

      @staticmethod

-     def post_receive(session, username, project, repotype, repodir, changes):

+     def post_receive(

+         session, username, project, repotype, repodir, changes, pull_request

+     ):

          """Run the post-receive tasks of a hook.

  

          For args, see BaseRunner.runhook.

@@ -65,7 +65,9 @@ 

      """ Runner for the hook blocking unsigned commits. """

  

      @staticmethod

-     def pre_receive(session, username, project, repotype, repodir, changes):

+     def pre_receive(

+         session, username, project, repotype, repodir, changes, pull_request

+     ):

          """Run the pre-receive tasks of a hook.

  

          For args, see BaseRunner.runhook.

file modified
+4 -2
@@ -89,7 +89,7 @@ 

  If you specify one or more branches (using commas `,` to separate them) only

  pushes made to these branches will trigger a new build of the documentation.

  

- To set up this hook, you will need to login to https://readthedocs.org/

+ To set up this hook, you will need to log in to https://readthedocs.org/

  Go to your project's admin settings, and in the ``Integrations`` section

  add a new ``Generic API incoming webhook``.

  
@@ -101,7 +101,9 @@ 

  

  class RtdRunner(BaseRunner):

      @staticmethod

-     def post_receive(session, username, project, repotype, repodir, changes):

+     def post_receive(

+         session, username, project, repotype, repodir, changes, pull_request

+     ):

          """Perform the RTD Post Receive hook.

  

          For arguments, see BaseRunner.runhook.

file modified
+40 -4
@@ -20,6 +20,7 @@ 

  import logging

  import os

  import shutil

+ import stat

  import subprocess

  import requests

  import tempfile
@@ -1262,7 +1263,15 @@ 

  

          new_repo.checkout("refs/heads/%s" % branch)

  

-         file_path = os.path.join(newpath, filename)

+         # Resolve path to identify path traversal and symlinks

+         file_path = os.path.realpath(os.path.join(newpath, filename))

+         # Bail out of file path is outside temp repo or inside the .git/ folder

+         # Avoids data leak and unauthorized changes in files or git config.

+         if (

+             not file_path.startswith(newpath)

+             or os.path.join(newpath, ".git") in file_path

+         ):

+             return

  

          # Get the current index

          index = new_repo.index
@@ -1481,6 +1490,20 @@ 

      return subject

  

  

+ def get_changed_files(torev, fromrev, abspath):

+     """Return files changed between HEAD and BASE.

+     Return as a dict with paths as keys and status letters as values.

+     """

+     if set(fromrev) == set("0") or set(fromrev) == set("^0"):

+         # get hash of the empty tree

+         cmd = ["hash-object", "-t", "tree", "/dev/null"]

+         fromrev = pagure.lib.git.read_git_output(cmd, abspath)

+     cmd = ["diff", "--name-status", "-z", fromrev, torev]

+     output = pagure.lib.git.read_git_output(cmd, abspath)

+     items = output.split("\0")

+     return {k: v for v, k in zip(items[0::2], items[1::2])}

+ 

+ 

  def get_repo_info_from_path(gitdir, hide_notfound=False):

      """Returns the name, username, namespace and type of a git directory

  
@@ -2271,7 +2294,7 @@ 

      _log.debug("pagure.lib.git.diff_pull_request, started")

      diff = None

      diff_commits = []

-     diff, diff_commits, _ = get_diff_info(

+     diff, diff_commits, orig_commit = get_diff_info(

          repo_obj,

          orig_repo,

          request.branch_from,
@@ -2317,7 +2340,10 @@ 

                  and request.commit_start != first_commit.oid.hex

              ):

                  pr_action = "rebased"

-                 commenttext = "rebased onto %s" % first_commit.oid.hex

+                 if orig_commit:

+                     commenttext = "rebased onto %s" % orig_commit.oid.hex

+                 else:

+                     commenttext = "rebased onto unknown target"

          request.commit_start = first_commit.oid.hex

          request.commit_stop = diff_commits[0].oid.hex

          session.add(request)
@@ -3053,7 +3079,17 @@ 

              def addToZip(zf, path, zippath):

                  if _exclude_git(path):

                      return

-                 if os.path.isfile(path):

+                 # if path is symlink, add actual link and not target to zip

+                 if os.path.islink(path):

+                     zi = zipfile.ZipInfo.from_file(path, zippath)

+                     zi.compress_type = zipfile.ZIP_DEFLATED

+                     # System which created zip, 3 = Unix; 0 = Windows

+                     zi.create_system = 3

+                     # mark as a symlink

+                     zi.external_attr |= stat.S_IFLNK << 16

+                     zf.writestr(zi, path)

+                     return

+                 elif os.path.isfile(path):

                      zf.write(path, zippath, zipfile.ZIP_DEFLATED)

                  elif os.path.isdir(path):

                      if zippath:

file modified
+1 -1
@@ -161,7 +161,7 @@ 

          return

  

      mqtt_host = pagure_config.get("MQTT_HOST")

-     mqtt_port = pagure_config.get("MQTT_PORT")

+     mqtt_port = int(pagure_config.get("MQTT_PORT"))

  

      mqtt_username = pagure_config.get("MQTT_USERNAME")

      mqtt_pass = pagure_config.get("MQTT_PASSWORD")

file modified
+14 -5
@@ -108,10 +108,19 @@ 

      pass

  

  

- def set_redis(host, port, dbname):

-     """ Set the redis connection with the specified information. """

+ def set_redis(host=None, port=None, socket=None, dbname=0):

+     """Set the redis connection with the specified information."""

      global REDIS

-     pool = redis.ConnectionPool(host=host, port=port, db=dbname)

+     if socket:

+         pool = redis.ConnectionPool(

+             connection_class=redis.UnixDomainSocketConnection, path=socket

+         )

+     elif host and port:

+         pool = redis.ConnectionPool(host=host, port=port, db=dbname)

+     else:

+         raise pagure.exceptions.PagureException(

+             "Configure either REDIS_HOST and REDIS_PORT or REDIS_SOCKET"

+         )

      REDIS = redis.StrictRedis(connection_pool=pool)

  

  
@@ -4659,12 +4668,12 @@ 

          query = query.filter(model.PullRequest.date_created <= created_until)

  

      if updated_since:

-         query = query.filter(model.PullRequest.updated_on <= updated_since)

+         query = query.filter(model.PullRequest.updated_on >= updated_since)

      if updated_until:

          query = query.filter(model.PullRequest.updated_on <= updated_until)

  

      if closed_since:

-         query = query.filter(model.PullRequest.closed_at <= closed_since)

+         query = query.filter(model.PullRequest.closed_at >= closed_since)

      if closed_until:

          query = query.filter(model.PullRequest.closed_at <= closed_until)

  

file modified
+1
@@ -150,6 +150,7 @@ 

          cmd = ["git", "log"]

          if log_options:

              cmd.extend(log_options)

+         cmd.append("--end-of-options")

          if fromref:

              cmd.append(fromref)

          if target:

file modified
+11 -2
@@ -48,8 +48,17 @@ 

      broker_url = os.environ["PAGURE_BROKER_URL"]

  elif pagure_config.get("BROKER_URL"):

      broker_url = pagure_config["BROKER_URL"]

- else:

-     broker_url = "redis://%s" % pagure_config["REDIS_HOST"]

+ elif pagure_config.get("REDIS_SOCKET"):

+     broker_url = "redis+socket://%s?virtual_host=%d" % (

+         pagure_config["REDIS_SOCKET"],

+         pagure_config["REDIS_DB"],

+     )

+ elif "REDIS_HOST" in pagure_config and "REDIS_PORT" in pagure_config:

+     broker_url = "redis://%s:%d/%d" % (

+         pagure_config["REDIS_HOST"],

+         pagure_config["REDIS_PORT"],

+         pagure_config["REDIS_DB"],

+     )

  

  conn = Celery("tasks", broker=broker_url, backend=broker_url)

  conn.conf.update(pagure_config["CELERY_CONFIG"])

file modified
+11 -2
@@ -39,8 +39,17 @@ 

      broker_url = os.environ["PAGURE_BROKER_URL"]

  elif pagure_config.get("BROKER_URL"):

      broker_url = pagure_config["BROKER_URL"]

- else:

-     broker_url = "redis://%s" % pagure_config["REDIS_HOST"]

+ elif pagure_config.get("REDIS_SOCKET"):

+     broker_url = "redis+socket://%s?virtual_host=%d" % (

+         pagure_config["REDIS_SOCKET"],

+         pagure_config["REDIS_DB"],

+     )

+ elif "REDIS_HOST" in pagure_config and "REDIS_PORT" in pagure_config:

+     broker_url = "redis://%s:%d/%d" % (

+         pagure_config["REDIS_HOST"],

+         pagure_config["REDIS_PORT"],

+         pagure_config["REDIS_DB"],

+     )

  

  conn = Celery("tasks_mirror", broker=broker_url, backend=broker_url)

  conn.conf.update(pagure_config["CELERY_CONFIG"])

file modified
+11 -2
@@ -44,8 +44,17 @@ 

      broker_url = os.environ["PAGURE_BROKER_URL"]

  elif pagure_config.get("BROKER_URL"):

      broker_url = pagure_config["BROKER_URL"]

- else:

-     broker_url = "redis://%s" % pagure_config["REDIS_HOST"]

+ elif pagure_config.get("REDIS_SOCKET"):

+     broker_url = "redis+socket://%s?virtual_host=%d" % (

+         pagure_config["REDIS_SOCKET"],

+         pagure_config["REDIS_DB"],

+     )

+ elif "REDIS_HOST" in pagure_config and "REDIS_PORT" in pagure_config:

+     broker_url = "redis://%s:%d/%d" % (

+         pagure_config["REDIS_HOST"],

+         pagure_config["REDIS_PORT"],

+         pagure_config["REDIS_DB"],

+     )

  

  conn = Celery("tasks", broker=broker_url, backend=broker_url)

  conn.conf.update(pagure_config["CELERY_CONFIG"])

@@ -52,6 +52,6 @@ 

      maxCount: 10

    }

    ],{

-     footer: '<a href="http://www.emoji.codes" target="_blank">Browse All<span class="arrow">&raquo;</span></a>'

+     footer: '<a href="https://github.com/markdown-templates/markdown-emojis" target="_blank">Browse All<span class="arrow">&raquo;</span></a>'

    });

  };

file modified
+1 -1
@@ -172,7 +172,7 @@ 

      + '              <button class="reply btn btn-outline-primary border-0" type="button"'

      + '                  data-comment="' + data.comment_id + '"'

      + '                  title="Reply to this comment">'

-     + '                <span class="fa fa-share-square-o" title="Reply to this comment"></span>'

+     + '                <span class="fa fa-reply" title="Reply to this comment"></span>'

      + '              </a>';

      if ( data.comment_user == username) {

  

@@ -2,17 +2,17 @@ 

  <p class="justify">

  <strong>Ticket</strong>: A user or a group with this level of access can only edit metadata

    of an issue. This includes changing the status of an issue, adding/removing

-   tags from them, adding/removing assignees and every other option which can

-   be accessed when you click "Edit Metadata" button in an issue page. However,

-   this user can not "create" a new tag or "delete" an existing tag because,

-   that would involve access to settings page of the project which this user

-   won't have. It also won't be able to "delete" the issue because, it falls

+   tags, adding/removing assignees and every other option which can be accessed

+   when you click "Edit Metadata" button in an issue page. However, this user

+   can not "create" a new tag or "delete" an existing tag because that requires

+   access to the settings page of the project, which this user cannot do.  The

+   user also won't be able to "delete" the issue, because that action falls

    outside of "Edit Metadata".

  </p>

  <p class="justify">

- <strong>Collaborator</strong>: A user or a group with this level of access can do everything what

+ <strong>Collaborator</strong>: A user or a group with this level of access can do everything that

    a user/group with ticket access can do + it can commit to some branches in the project.

-   These branches are defined here using their name or a pattern and needs to be comma separated. <br />

+   These branches are defined here using their name or a pattern and need to be comma separated. <br />

    Some examples:

      <ul>

          <li>main,features/*</li>
@@ -21,14 +21,14 @@ 

      </ul>

  </p>

  <p class="justify">

- <strong>Commit</strong>: A user or a group with this level of access can do everything what

-   a user/group with ticket access can do + it can do everything on the project

-   which doesn't include access to settings page. It can "Edit Metadata" of an issue

-   just like a user with ticket access would do, can merge a pull request, can push

+ <strong>Commit</strong>: A user or a group with this level of access can do everything that

+   a user/group with ticket access can do + it can take any action on the project

+   that doesn't include access to the settings page. It can "Edit Metadata" of an

+   issue just like a user with ticket access, can merge a pull request, can push

    to the main repository directly, delete an issue, cancel a pull request etc.

  </p>

  <p class="justify">

- <strong>Admin</strong>: The user/group with this access has access to everything on the project.

-   All the "users" of the project that have been added till now are having this access.

-   They can change the settings of the project, add/remove users/groups on the project.

+ <strong>Admin</strong>: A user/group with this level of access has access to everything in the project.

+   All the "users" of the project that have been added until now have this access.

+   They can change the settings of the project and add/remove users/groups on the project.

  </p>

@@ -162,7 +162,7 @@ 

                  <button class="reply btn btn-outline-primary border-0" type="button"

                      data-comment="{{ comment.id }}"

                      title="Reply to this comment">

-                 <span class="fa fa-share-square-o"></span>

+                 <span class="fa fa-reply"></span>

              </button>

              {% endif %}

              {% if id != 0 and g.fas_user and (g.repo_committer or (
@@ -253,7 +253,7 @@ 

              {% if g.fas_user %}

                <a class="reply btn btn-outline-primary border-0 pointer" data-toggle="tooltip"

                    title="Reply to this comment">

-                 <span class="fa fa-share-square-o"></span>

+                 <span class="fa fa-reply"></span>

                </a>

              {% endif %}

              {% if g.fas_user and (g.repo_committer or g.fas_user.username == pull_request.user.username) %}
@@ -317,7 +317,7 @@ 

        {% if g.fas_user %}

          <a class="btn btn-outline-secondary border-0 btn-sm reply pointer"

            title="Reply to the initial comment - lose formatting">

-           <i class="fa fa-share-square-o"></i> Reply

+           <i class="fa fa-reply"></i> Reply

          </a>

        {% endif %}

      </div>

file modified
+1 -1
@@ -20,7 +20,7 @@ 

  {# we recognize non-executable file, executable file and symlink #}

  {% set expected_modes = [33188, 33261, 40960] %}

  <div class="row">

-   <div class="col">

+   <div class="col-2">

        {% block overviewtabs %}{{ super() }}{% endblock %}

    </div>

    <div class="col-10">

@@ -115,6 +115,7 @@ 

                          {{ diff_commit_full.author | author2avatar(20) | safe }}

                          {{ diff_commit_full.author.name }}

                        <span class="commitdate"

+                             data-toggle="tooltip"

                        title="{{ diff_commit_full.commit_time|format_ts }}"> &bull;

                        {{ diff_commit_full.commit_time|humanize }}</span>&nbsp;&nbsp;

                      </div>
@@ -172,6 +173,7 @@ 

                            author=commit.author.email),

                        cssclass="notblue")|safe}}

                        <span class="commitdate"

+                             data-toggle="tooltip"

                        title="{{ commit.commit_time|format_ts }}"> &bull;

                      {{ commit.commit_time|humanize }}</span>&nbsp;&nbsp;

                        <span id="commit-actions"></span>

@@ -99,7 +99,7 @@ 

  });

  

  $('.give_group_btn').click(function() {

-   return confirm('Are you sure to give {{ group.group_name }}? \nThis is final and cannot be un-done.');

+   return confirm('Are you sure you want to give {{ group.group_name }}? \nThis is final and cannot be undone.');

  })

  </script>

  {% endblock %}

file modified
+25 -18
@@ -16,7 +16,9 @@ 

  </style>

  {% endblock %}

  

+ 

  {% block repo %}

+   <!-- template: file.html -->

    <div class="row">

      <div class="col-2">

          {% block overviewtabs %}{{ super() }}{% endblock %}
@@ -29,14 +31,13 @@ 

      </h3>

      </div>

  

-     <div class="col-sm-6">

-       <div class="float-right">

+     <div class="col-sm-6 text-right">

          {% if branchname in g.branches %}

            <div class="btn-group">

              <a href="#" class="btn btn-outline-light border-secondary text-dark btn-sm dropdown-toggle"

                      data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">

                      <span class="fa fa-random fa-fw"></span> Branch: <span class="font-weight-bold">{{ branchname }}</span>

-           </a>

+             </a>

              <div class="dropdown-menu dropdown-menu-right">

                {% for branch in g.branches %}

                    <a class="dropdown-item pl-1 {{'active' if branchname == branch}}" href="{{ url_for(
@@ -58,7 +59,7 @@ 

          {% endif %}

      </div>

    </div>

-   </div>

+     <!-- .card -->

      <div class="card mb-3">

        <div class="card-header">

          <ol class="breadcrumb p-0 bg-transparent mb-0">
@@ -94,12 +95,12 @@ 

          </ol>

        </div>

  

- {% if content is not none %}

-   {% if output_type in ('file','binary','image','markup') %}

+ {% if content is none %}

+   No content found in this repository

+ {% else %}

    <div class="card-body p-0">

-         {% if content is not none %}

-           {% if output_type in ('file','binary','image','markup') %}

-             <div class="bg-light border text-right pr-2">

+   {% if output_type in ('file','binary','image','markup') %}

+             <div class="bg-light border text-right pr-2 py-1">

                  {% if output_type in ('file','markup') and g.repo_admin %}

                  <a class="btn btn-sm btn-secondary" href="{{ url_for(

                      'ui_ns.edit_file',
@@ -121,7 +122,7 @@ 

                          )

                      )

                  %}

-                 <form class="btn btn-sm" method="POST" name="fork_project"

+                 <form class="d-inline mx-2" method="POST" name="fork_project"

                      action="{{ url_for('ui_ns.fork_edit_file',

                          repo=repo.name,

                          username=username,
@@ -178,8 +179,6 @@ 

                      identifier=branchname,

                      filename=filename) | unicode }}" title="View as raw">Raw</a>

              </div>

-           {% endif %}

-         {% endif %}

  

      {% if output_type=='file' %}

          <pre class="syntaxhighlightblock"><code class="{{filename|syntax_alias}}">{{ content }}</code></pre>
@@ -209,10 +208,18 @@ 

            </a>

          </p>

      {% endif %}

-   </div>

    {% else %}

-       <div class="card-body p-0">

-           <table class="table table-sm mb-0">

+       <div class="bg-light border text-right pr-2 py-1">

+           <a class="btn btn-secondary btn-sm" href="{{ url_for(

+                     'ui_ns.view_history_file',

+                     repo=repo.name,

+                     username=username,

+                     namespace=repo.namespace,

+                     identifier=branchname,

+                     filename=filename) | unicode }}" title="View git log for this path">History</a>

+ 

+       </div>

+       <table class="table table-sm mb-0">

          <tbody>

            {% for entry in content %}

              <tr>
@@ -243,10 +250,8 @@ 

            {% endfor %}

          </tbody>

        </table>

-       </div>

    {% endif %}

- {% else %}

- No content found in this repository

+   </div>

  {% endif %}

   </div> <!-- end .card-->

  
@@ -262,8 +267,10 @@ 

   {% endif %}

  </div>

  </div>

+ <!-- /template: file.html -->

  {% endblock %}

  

+ 

  {% block jscripts %}

  {{ super() }}

  <script type="text/javascript" nonce="{{ g.nonce }}" src="{{

@@ -63,7 +63,7 @@ 

      <div class="card">

        <div class="card-header">

          <ol class="breadcrumb p-0 bg-transparent mb-0">

-           <li>

+           <li class="breadcrumb-item">

              <a href="{{ url_for('ui_ns.view_tree',

                  repo=repo.name,

                  username=username,
@@ -74,28 +74,22 @@ 

                </span>&nbsp; {{ branchname }}

              </a>

            </li>

-         {% set path = '' %}

          {% for file in filename.split('/') %}

-           {% if loop.first %}

-           {% set path = file %}

+           {% set path = '/'.join(filename.split('/')[:loop.index]) %}

+           {% if loop.last %}

+           {% set path_type = 'file' %}

            {% else %}

-           {% set path = path + '/' + file %}

+           {% set path_type = 'folder' %}

            {% endif %}

-           {% if loop.index != loop.length %}<li><a

-           href="{{ url_for('ui_ns.view_file',

+           <li class="breadcrumb-item">

+             <a href="{{ url_for('ui_ns.view_file',

                  repo=repo.name,

                  username=username,

                  namespace=repo.namespace,

                  identifier=branchname,

                  filename=path | unicode)}}">

-             <span class="fa fa-folder"></span>&nbsp; {{ file }}</a>

+             <span class="fa fa-{{ path_type }}"></span>&nbsp; {{ file }}</a>

            </li>

-           {% elif file %}

-           <li class="active">

-             <span class="fa {% if output_type == 'tree' %}fa-folder{% else %}fa-file{% endif %}">

-             </span>&nbsp; {{ file }}

-           </li>

-           {% endif %}

          {% endfor %}

          </ol>

        </div>
@@ -128,6 +122,7 @@ 

                            author=commit.author.email),

                        cssclass="notblue")|safe}}

                        <span class="commitdate"

+                             data-toggle="tooltip"

                        title="{{ commit.commit_time|format_ts }}"> &bull;

                      {{ commit.commit_time|humanize }}</span>&nbsp;&nbsp;

                        <span id="commit-actions"></span>

@@ -128,7 +128,7 @@ 

          return confirm('Are you sure you want to delete the group `{{ group.group_name }}`?');

      })

      $('.remove-user-btn').click(function() {

-         return confirm('Are you sure to remove user `' + $(this).attr('data-username') + '` from the group `{{ group.group_name }}`?');

+         return confirm('Are you sure you want to remove user `' + $(this).attr('data-username') + '` from the group `{{ group.group_name }}`?');

      })

      $('#headerSearch').on('keypress keydown keyup', function(e) {

        if (e.which == 13) {

file modified
+1 -1
@@ -32,7 +32,7 @@ 

    <div class="container mt-5">

      {{ render_repos(

          repos, total_page, 'page', page,

-         'All '+projectstring()+'s', repos_length, 'repos', username, sorting=sorting, select=select) }}

+         'All '+projectstring(plural=True), repos_length, 'repos', username, sorting=sorting, select=select) }}

    </div>

  

  {% endblock %}

file modified
+5 -5
@@ -97,7 +97,7 @@ 

              <span class="text-muted" data-toggle="tooltip" title="{{issue.date_created | format_datetime}}">

                  <span class="font-weight-bold">Opened</span> {{ issue.date_created |humanize }}

              </span>

-             <span class="text-muted" title="{{ issue.user.html_title }}">by {{ issue.user.user }}.</span>

+             <span class="text-muted" data-toggle="tooltip" title="{{ issue.user.html_title }}">by {{ issue.user.user }}.</span>

            {% endif %}

            </small>

          </div>
@@ -263,7 +263,7 @@ 

          </p>

        {% else %}

          <p>

-           <a href="{{ url_for('auth_login', next=request.url) }}">Login</a>

+           <a href="{{ url_for('auth_login', next=request.url) }}">Log in</a>

            to comment on this ticket.

          </p>

        {% endif %}
@@ -845,7 +845,7 @@ 

          'value': '{{ form.csrf_token.current_token }}',

          'type': 'hidden'

      })).appendTo('body');

-     if (confirm('Are you sure to delete this ticket? \nThis is final and cannot be un-done.')){

+     if (confirm('Are you sure you want to delete this ticket? \nThis is final and cannot be undone.')){

        closeForm.submit();

      }

      return false;
@@ -1009,7 +1009,7 @@ 

        setup_btn_take_drop();

      }

    ).fail(function() {

-     alert( "An error occured, could not assign this ticket to you." );

+     alert( "An error occurred, could not assign this ticket to you." );

    })

    return false;

  }
@@ -1034,7 +1034,7 @@ 

        setup_btn_take_drop();

      }

    ).fail(function() {

-     alert( "An error occured, could not drop the current assignee." );

+     alert( "An error occurred, could not drop the current assignee." );

    })

    return false;

  }

@@ -39,6 +39,8 @@ 

            </li>

            {% if (config.get('ENABLE_NEW_PROJECTS', True) and config.get('ENABLE_UI_NEW_PROJECTS', True))

            or config.get('ENABLE_GROUP_MNGT', False)  %}

+           {#can_create is only defined if using OIDC so assume we cancreate #}

+           {% if (g.fas_user.can_create is not defined) or (g.fas_user.can_create is true)%}

            <li class="nav-item dropdown ml-3">

              <a class="nav-link dropdown-toggle font-weight-bold"

                data-toggle="dropdown"
@@ -61,6 +63,7 @@ 

              </div>

            </li>

            {% endif %}

+           {% endif %}

            <li class="nav-item dropdown ml-3">

              <a class="nav-link dropdown-toggle" data-toggle="dropdown"

                href="#" role="button" aria-haspopup="true" aria-expanded="false">

@@ -35,5 +35,5 @@ 

  </section>

  

  {% else %}

- <p><a href="{{ url_for('auth_login') }}">Login</a> to comment on this ticket.</p>

+ <p><a href="{{ url_for('auth_login') }}">Log in</a> to comment on this ticket.</p>

  {% endif %}

@@ -120,7 +120,7 @@ 

  

    $(".delete-branch-form").submit(function() {

      console.log($(this));

-     return confirm('Are you sure you want to remove the branch: ' + $(this).attr("data-branch-name") + '?\nThis cannot be un-done!');

+     return confirm('Are you sure you want to remove the branch: ' + $(this).attr("data-branch-name") + '?\nThis cannot be undone!');

    });

  

    var _cnt = 0;

@@ -95,6 +95,7 @@ 

                        author=commit.author.email),

                    cssclass="notblue")|safe}}

                    <span class="commitdate"

+                         data-toggle="tooltip"

                    title="{{ commit.commit_time|format_ts }}"> &bull;

                  {{ commit.commit_time|humanize }}</span>&nbsp;&nbsp;

              </div>

@@ -210,7 +210,7 @@ 

                <input checked id="allow_rebase" name="allow_rebase" type="checkbox" value="y">

              </label>

              <small class="text-muted">

-               Let the maintainer of the target project to rebase the pull-request

+               Allow the maintainer of the target project to rebase the pull-request

              </small>

            </div>

          </div>
@@ -290,6 +290,7 @@ 

                            author=commit.author.email),

                        cssclass="notblue")|safe}}

                        <span class="commitdate"

+                             data-toggle="tooltip"

                        title="{{ commit.commit_time|format_ts }}"> &bull;

                      {{ commit.commit_time|humanize }}</span>&nbsp;&nbsp;

                  </div>

@@ -68,7 +68,7 @@ 

              <span class="text-muted" data-toggle="tooltip" title="{{pull_request.date_created | format_datetime}}">

                  <span class="font-weight-bold">Opened</span> {{ pull_request.date_created |humanize }}

              </span>

-             <span class="text-muted" title="{{ pull_request.user.html_title }}">by {{ pull_request.user.user }}.</span>

+             <span class="text-muted" data-toggle="tooltip" title="{{ pull_request.user.html_title }}">by {{ pull_request.user.user }}.</span>

            {% elif pull_request.status == 'Closed' %}

              <span data-toggle="tooltip" title="{{pull_request.closed_at | format_datetime}}">

                <span class="text-danger font-weight-bold">Closed</span> {{ pull_request.closed_at |humanize }}
@@ -78,7 +78,7 @@ 

              <span class="text-muted" data-toggle="tooltip" title="{{pull_request.date_created | format_datetime}}">

                  <span class="font-weight-bold">Opened</span> {{ pull_request.date_created |humanize }}

              </span>

-             <span class="text-muted" title="{{ pull_request.user.html_title }}">by {{ pull_request.user.user }}.</span>

+             <span class="text-muted" data-toggle="tooltip" title="{{ pull_request.user.html_title }}">by {{ pull_request.user.user }}.</span>

            {% endif %}

            </small>

          </div>
@@ -170,7 +170,7 @@ 

                </a>

                <div id="merge-alert" class="text-xs-center dropdown-menu dropdown-menu-right p-0">

                  <div class="alert text-center mb-0">

-                   {% if g.repo_committer %}

+                   {% if g.authenticated and (g.repo_committer or g.fas_user.username in g.repo.collaborators) %}

                      <small id="merge-alert-message"></small>

                      <form action="{{ url_for('ui_ns.merge_request_pull',

                          repo=repo.name,
@@ -342,6 +342,7 @@ 

                            author=commit.author.email),

                        cssclass="notblue")|safe}}

                        <span class="commitdate"

+                             data-toggle="tooltip"

                        title="{{ commit.commit_time|format_ts }}"> &bull;

                      {{ commit.commit_time|humanize }}</span>&nbsp;&nbsp;

                  </div>

file modified
+22 -22
@@ -151,7 +151,7 @@ 

                    <label for="tags">Project tags</label>

                    <input class="form-control" name="tags" value="{{ repo.tags_text |join(', ') if repo.tags else '' }}" />

                    <small class="text-muted">

-                     Tags for project itself, as a comma-separated list. Tags

+                     Tags for the project itself, as a comma-separated list. Tags

                      for issues are managed further down on this page.

                    </small>

                  </fieldset>
@@ -192,11 +192,11 @@ 

                <div class="row">

                  <div class="col">

                      <p>

-                         Each message sent to the web-hook are signed via hmac and SHA1 using

+                         Each message sent to the web-hook is signed via hmac and SHA1 using

                          this private key.

                        </p>

                        <p>

-                         This key is private to your project, make sure to store in a safe place

+                         This key is private to your project.  Be sure to store it in a safe place

                          and do not share it.

                        </p>

                        <div class="form-group">
@@ -213,7 +213,7 @@ 

                            method="post" class="icon">

                        <button class="btn btn-primary" type="submit" id="generate_new_hook_token"

                          title="Generate a new hook token">

-                         <span class="fa fa-refresh"></span> &nbsp;Re-generate

+                         <span class="fa fa-refresh"></span> &nbsp;Regenerate

                        </button>

                        {{ form.csrf_token }}

                      </form>
@@ -239,7 +239,7 @@ 

                      <p>

                          The email addresses entered below will receive all the notifications

                          related to {% if g.issues_enabled %}

-                         (public) issues and {% endif %}pull-requests, this includes

+                         (public) issues and {% endif %}pull-requests. This includes

                          notifications about {% if g.issues_enabled %}

                          new issue or {% endif %} new pull-request, new comment

                          and status change.
@@ -511,7 +511,7 @@ 

                      <p>

                          Below are the priorities you may assign to a ticket, allowing you

                          to sort them with it. The Weight determines the ordering. Higher

-                         priority should correspond to lower weight.

+                         priority corresponds to lower weight.

                          <span class="italic">

                            To remove an entry, simply clean the Weight and Title

                          </span>
@@ -576,7 +576,7 @@ 

                  <div class="row">

                    <div class="col">

                            <p>

-                             The default priority will be set to all issues created after

+                             The default priority will be set on all issues created after

                              it has been set.

                            </p>

                          <form action="{{ url_for(
@@ -623,7 +623,7 @@ 

                  <div class="row">

                    <div class="col">

                        <p>

-                           Here is the list of all the status that can be used when closing

+                           Here is the list of all statuses that can be used when closing

                            an issue.

                          </p>

                        <form action="{{ url_for(
@@ -693,7 +693,7 @@ 

                      <div class="col">

                          <p>

                              Set some custom fields for your issues.  <i>Field Values</i> are currently

-                             only used for Lists, and it accepts a comma separated list of items

+                             only used for Lists, and consist of a comma separated list of items

                              for the drop down list.

                            </p>

                          <form action="{{ url_for(
@@ -939,7 +939,7 @@ 

                </h3>

                <div class="row">

                  <div class="col">

-                     <p>Quick replies will be offered in a new comment form on Issue or

+                     <p>Quick replies will be offered in a new comment form on the Issue or

                          Pull Request page. This allows you to reply to common problems with a

                          click of a button.</p>

                          <p>The reply can use the same Markdown formatting as regular
@@ -1138,21 +1138,21 @@ 

      updateform();

  

      $('#generate_new_hook_token').click(function() {

-         return confirm('Are you sure to generate a new token for '

-                           + 'this project/fork? \nThis will break all web hook in place and '

-                           + 'cannot be un-done.');

+         return confirm('Are you sure you want to generate a new token for '

+                           + 'this project/fork? \nThis will break all web hooks in place and '

+                           + 'cannot be undone.');

      });

  

      $('.remove_user_btn').click(function(){

-       return confirm('You sure you want to remove this user from this project?');

+       return confirm('Are you sure you want to remove this user from this project?');

      });

  

      $('.remove_group_btn').click(function(){

-       return confirm('You sure you want to remove this group from this project?');

+       return confirm('Are you sure you want to remove this group from this project?');

      });

  

      $('.remove_deploy_key_btn').click(function(){

-       return confirm('You sure you want to remove this deploy key from this project?');

+       return confirm('Are you sure you want to remove this deploy key from this project?');

      });

  

      $('.delete_report_btn').click(function(){
@@ -1165,21 +1165,21 @@ 

      });

  

      $('.give_project_btn').click(function(){

-       return confirm('Are you sure to give {{ repo.fullname }}? \nThis is final and cannot be un-done.');

+       return confirm('Are you sure you want to give {{ repo.fullname }}? \nThis is final and cannot be undone.');

      });

  

      $('.delete_project_btn').click(function(){

-       return confirm('Are you sure to delete {{ repo.fullname }}? \nThis is final and cannot be un-done.');

+       return confirm('Are you sure you want to delete {{ repo.fullname }}? \nThis is final and cannot be undone.');

      });

  

      $('.revoke_token_btn').click(function(){

-       return confirm('Are you sure to revoke this token ?'

-                      + '\nThis will break all application using it and '

-                      + 'cannot be un-done.');

+       return confirm('Are you sure you want to revoke this token ?'

+                      + '\nThis will break all applications using it and '

+                      + 'cannot be undone.');

      });

  

      $('.renew_token_btn').click(function(){

-       return confirm('Are you sure to renew this token ?'

+       return confirm('Are you sure you want to renew this token ?'

                       + '\nIt will have the same ACL but will be a different key.');

      });

  

@@ -29,16 +29,16 @@ 

    <div class="col">

      <p>

        API keys are tokens used to authenticate you on pagure. They can also

-       be used to grant access to 3rd party application to behave on this

-       project on your name.

+       be used to grant access to 3rd party applications to act on this

+       project on your behalf.

      </p>

      <p>

        These are your personal tokens; they are not visible to the other

        admins of this repository.

      </p>

      <p>

-       These keys are private to your project, make sure to store in a safe

-       place and do not share it.

+       These keys are private to your project. Be sure to store them in a safe

+       place and do not share them.

      </p>

      {% if repo.tokens %}

      {% for token in repo.tokens %}

@@ -15,7 +15,7 @@ 

        <p>{{ error.description }}</p>

        </div>

        {% endif %}

-       <p>If you're not logged in, try to login; if you're already done,

+       <p>If you're not logged in, try to log in; if you've already done so,

        that probably means you do not have sufficient access.</p>

      </div>

    </div>

@@ -7,6 +7,7 @@ 

  {% set tag = "users"%}

  

  {% macro render_email(email, form, validated=True) %}

+ {% set random_number = range(0, 256) | random() %}

  <div class="list-group-item {% if not validated %}disabled{% endif %}">

    <span class="fa fa-envelope text-muted"></span> &nbsp;{{ email.email }}

    {% if validated %}
@@ -25,11 +26,11 @@ 

      </div>

      {% else %}

      <form class="inline" method="POST"

-       action="{{ url_for('ui_ns.set_default_email') }}" id="default_mail">

+       action="{{ url_for('ui_ns.set_default_email') }}" id="default_mail_{{ random_number }}">

        <input type="hidden" value="{{ email.email }}" name="email" />

        {{ form.csrf_token }}

        <a class="float-right p-r-1 btn btn-outline-warning border-0 text-secondary mr-1 pointer submit-btn"

-          data-form-id="default_mail" title="Set as default email address">

+          data-form-id="default_mail_{{ random_number }}" title="Set as default email address">

           <span class="fa fa-star" data-toggle="tooltip"></span>

        </a>

      </form>
@@ -84,7 +85,7 @@ 

                <fieldset class="form-group text-center">

                  <div>

                    <div class="p-2 mt-2 bg-light border border-secondary"> {{ g.fas_user.username | avatar(80) | safe }} </div>

-                   <a class="btn btn-outline-primary btn-sm mt-1" href="https://www.libravatar.org/account/login/">

+                   <a class="btn btn-outline-primary btn-sm mt-1" href="https://www.libravatar.org/accounts/login/">

                    Change Avatar </a>

                  </div>

                </fieldset>
@@ -147,15 +148,15 @@ 

              <div class="col">

                <p>

                  API keys are tokens used to authenticate you on pagure. They can also

-                 be used to grant access to 3rd party application to behave on all

+                 be used to grant access to 3rd party applications to act on all

                  {{projectstring(plural=True)}} in your name.

                </p>

                <p>

                  These are your personal tokens; they are not visible to others.

                </p>

                <p>

-                 These keys are private, make sure to store in a safe place and

-                 do not share it.

+                 These keys are private. Be sure to store them in a safe place and

+                 do not share them.

                </p>

                {% if user.tokens %}

                {% for token in user.tokens %}
@@ -273,7 +274,7 @@ 

                  <div class="row">

                    <div class="col">

                        <p>

-                           Forcefully log out from every current open session.

+                           Forcefully log out from every currently open session.

                        </p>

                        <form action="{{ url_for('ui_ns.force_logout') }}" method="post">

                          <input type="submit" class="btn btn-outline-danger"
@@ -300,15 +301,15 @@ 

          $('#' + _form_name).submit();

      });

      $('.remove-token-btn').click(function() {

-       return confirm('Are you sure to revoke this token ?'

-                      + '\nThis will break all application using it and '

-                      + 'cannot be un-done.');

+       return confirm('Are you sure you want to revoke this token ?'

+                      + '\nThis will break all applications using it and '

+                      + 'cannot be undone.');

      })

      $('.delete-email-btn').click(function() {

        return confirm('Do you really want to remove the email: ' + $(this).attr('data-email') + '?');

      })

      $('.delete-sshkey-btn').click(function() {

-       return confirm('You sure you want to remove this SSH key?');

+       return confirm('Are you sure you want to remove this SSH key?');

      })

  

      $('#nav-tab a.nav-link').on('shown.bs.tab', function (e) {

@@ -13,4 +13,9 @@ 

  

  .footer a.notblue{

    opacity:0.7;

- } 

\ No newline at end of file

+ }

+ 

+ .enforce-text-break {

+   word-wrap: break-word !important;

+   word-break: break-all !important;

+ }

@@ -109,7 +109,7 @@ 

                      group=group.group_name, user=user.user) }}">

                  {{ form.csrf_token }}

                  <button

-                   onclick="return confirm('Are you sure to remove user `{{

+                   onclick="return confirm('Are you sure you want to remove user `{{

                      user.user}}` from the group `{{group.group_name}}`?');"

                    title="Remove user from group"

                    class="btn btn-sm btn-outline-danger border-0">

@@ -137,46 +137,99 @@ 

  {% block jscripts %}

  {{ super() }}

  <script type="text/javascript" nonce="{{ g.nonce }}">

- function disable_branches(){

-   function get_branches(_url){

-     $.ajax({

-       url: _url,

-       type: 'GET',

-       dataType: 'json',

-       success: function(res) {

-         var act_br = $('#active_branches');

-         var inact_br = $('#inactive_branches');

-         for (branch in res.results){

-           branch = res.results[branch];

-           var _it = $('#branch-' + branch.name);

-           _it.addClass('disabled_branch');

-           inact_br.append(_it);

-           if (branch.name == 'master') {

-             var _t = $('.projectinfo');

-             html = ' \

-                 <div class="small label label-sm label-danger" data-toggle="tooltip" \

-                  title="This package has been retired">Retired on Fedora</div>';

-             _t.append(html);

+ function disable_branches() {

+   function checkForDeadPackage(branch, namespace, repo_name) {

+     return new Promise((resolve, reject) => {

+       // Getting variable from config to find out type of link that should be used

+       let appUrl = "{{ config['APP_URL'] }}"

+ 

+       let branchUrl;

+       if (appUrl === "https://stg.pagure.io/") {

+         branchUrl = `https://src.stg.fedoraproject.org/${namespace}/${repo_name}/blob/${branch}/f/dead.package`;

+       } else {

+         branchUrl = `https://src.fedoraproject.org/${namespace}/${repo_name}/blob/${branch}/f/dead.package`;

+       }

+       $.ajax({

+         url: branchUrl,

+         type: "HEAD",

+         success: function () {

+           resolve({ branch, hasDeadPackage: true });

+         },

+         error: function (error) {

+           if (error.status === 404) {

+             resolve({ branch, hasDeadPackage: false });

+           } else {

+             reject(`Error: ${error.status} on branch ${branch}`);

            }

          }

-         if (res.next){

-           get_branches(res.next);

+       });

+     });

+   }

+ 

+   function checkForDeadPackagesInBranches(branches, namespace, repo_name) {

+     const promises = branches.map(branch => checkForDeadPackage(branch, namespace, repo_name));

+     Promise.all(promises).then(results => {

+       let act_br = $('#active_branches');

+       let inact_br = $('#inactive_branches');

+       results.forEach(result => {

+         if (result.hasDeadPackage) {

+           console.log(_ns, _repo_name, result.branch);

+           let _it = $('#branch-' + result.branch);

+           _it.addClass('disabled_branch');

+           inact_br.append(_it);

          }

+       });

+     });

+   }

  

-         $('#active_branches_count').html($("#active_branches .list-group-item").length)

-         $('#inactive_branches_count').html($("#inactive_branches .list-group-item").length)

-       }

-     })

+   function fetchReleases(url) {

+     return $.ajax({

+       url: url,

+       method: 'GET',

+       dataType: 'json',

+     });

    }

-   var _ns = '{{ repo.namespace }}';

-   if (_ns == 'rpms') {

-     _ns = 'rpm';

-   } else if (_ns == 'modules') {

-     _ns = 'modules';

+ 

+   function getBranches() {

+     // Getting variable from config to find out type of link that should be used

+     let appUrl = "{{ config['APP_URL'] }}"

+ 

+     let request1, request2, request3;

+     if (appUrl === "https://stg.pagure.io/") {

+       request1 = fetchReleases('https://bodhi.stg.fedoraproject.org/list_releases/?state=current');

+       request2 = fetchReleases('https://bodhi.stg.fedoraproject.org/list_releases/?state=pending');

+       request3 = fetchReleases('https://bodhi.stg.fedoraproject.org/list_releases/?state=frozen');

+     } else {

+       request1 = fetchReleases('https://bodhi.fedoraproject.org/list_releases/?state=current');

+       request2 = fetchReleases('https://bodhi.fedoraproject.org/list_releases/?state=pending');

+       request3 = fetchReleases('https://bodhi.fedoraproject.org/list_releases/?state=frozen');

+     }

+ 

+     return Promise.all([request1, request2, request3]).then(function (responses) {

+       let currentReleases = responses[0].releases;

+       let pendingReleases = responses[1].releases;

+       let frozenReleases = responses[2].releases;

+ 

+       let mergedReleases = currentReleases.concat(pendingReleases).concat(frozenReleases);

+       let tags = mergedReleases.map(release => release.branch);

+ 

+       return [...new Set(tags)]; // Return the unique tags

+     }).catch(function (error) {

+       console.error('Error:', error);

+     });

    }

-   var _url = 'https://pdc.fedoraproject.org/rest_api/v1/component-branches/'

-         + '?active=false&type=' + _ns + '&global_component={{ repo.name }}';

-   get_branches(_url);

+ 

+ 

+   const _ns = '{{ repo.namespace }}'

+   const _repo_name = '{{ repo.name }}'

+   console.log(_ns, _repo_name);

+ 

+   getBranches().then(branches => {

+     console.log(branches)

+     checkForDeadPackagesInBranches(branches, _ns, _repo_name);

+   }).catch(function (error) {

+     console.error("Error:", error);

+   });

  }

  

  $(function() {

@@ -545,16 +545,16 @@ 

      });

  

      $.ajax({

- 	  url: 'https://transtats.fedoraproject.org/api/package/{{ repo.name }}/exist?format=json',

- 	  type: 'GET',

- 	  dataType: 'json',

- 	  success: function(res){

- 	     console.log(res);

- 	     if (res[{{ repo.name }}]) {

-               $("#transtats").html("<div class='btn-group'><a class='transtats' href='https://transtats.fedoraproject.org/packages/view/{{ repo.name }}'><button type='button' class='btn btn-sm btn-outline-primary font-weight-bold'>&nbsp;Translation Status</button></a></div>");

- 	     }

- 	  }

-     });

+  	  url: 'https://transtats.fedoraproject.org/api/package/' + repo.name + '/exist?format=json',

+  	  type: 'GET',

+  	  dataType: 'json',

+  	  success: function(res){

+  	     console.log(res);

+  	     if (res[repo.name]) {

+                $("#transtats").html("<div class='btn-group'><a class='transtats' href='https://transtats.fedoraproject.org/packages/view/" + repo.name + "'><button type='button' class='btn btn-sm btn-outline-primary font-weight-bold'>&nbsp;Translation Status</button></a></div>");

+  	     }

+  	  }

+      });

    {% endif %}

  

  });

@@ -1,4 +1,4 @@ 

- <nav class="nav nav-tabs nav-sidetabs flex-column">

+ <nav class="nav nav-tabs nav-sidetabs flex-column flex-nowrap">

    <a class=

        "nav-link nowrap

        {%if select == 'overview' %} active{% endif %}"
@@ -66,7 +66,7 @@ 

    </a>

  

    {% if 'distgit_ns' in g.main_app.blueprints and not repo.is_fork and repo.namespace != 'tests'%}

-   <div class="col-xs-2 line-height-1"></div>

+   <div class="line-height-1"></div>

    <h6>Monitoring status:</h6>

    <div class="btn-group">

      {% if g.authenticated %}
@@ -107,6 +107,46 @@ 

            </div>

          </div>

        </a>

+       <a class="dropdown-item pl-2" id="monitoring_all_option_button">

+         <div class="media">

+           <div class="align-self-center check-icon pr-2">

+             <span class="fa fa-fw"></span>

+           </div>

+           <div class="media-body">

+             Monitoring all

+           </div>

+         </div>

+       </a>

+       <a class="dropdown-item pl-2" id="monitoring_all_and_scratch_option_button">

+         <div class="media">

+           <div class="align-self-center check-icon pr-2">

+             <span class="fa fa-fw"></span>

+           </div>

+           <div class="media-body">

+             Monitoring all and scratch builds

+           </div>

+         </div>

+       </a>

+       <a class="dropdown-item pl-2" id="monitoring_stable_option_button">

+         <div class="media">

+           <div class="align-self-center check-icon pr-2">

+             <span class="fa fa-fw"></span>

+           </div>

+           <div class="media-body">

+             Monitoring stable only

+           </div>

+         </div>

+       </a>

+       <a class="dropdown-item pl-2" id="monitoring_stable_and_scratch_option_button">

+         <div class="media">

+           <div class="align-self-center check-icon pr-2">

+             <span class="fa fa-fw"></span>

+           </div>

+           <div class="media-body">

+             Monitoring stable only and scratch builds

+           </div>

+         </div>

+       </a>

      </div>

      <div id="monitoring_feedback"></div>

      {% else %}
@@ -118,9 +158,9 @@ 

      {% endif %}

    </div>

  

-   <div class="col-xs-2 line-height-1"></div>

-   <div id="orphan-section" class="pt-3">

-       <div class="col-xs-2 line-height-1"></div>

+   <div class="line-height-1"></div>

+   <div id="orphan-section" class="pt-3 enforce-text-break">

+       <div class="line-height-1"></div>

        {% if repo.user.user == "orphan" %}

        <p>Orphaned for: {{ repo.orphan_reason.reason }}

        {% if repo.orphan_reason.reason_info %}
@@ -154,7 +194,7 @@ 

    </div>

  

    <div class="pt-3">

-     <div class="col-xs-2 line-height-1">

+     <div class="line-height-1">

      <h6>Bugzilla Assignee:</h6>

        <dl>

          <dt>Fedora: </dt>
@@ -273,6 +313,18 @@ 

          } else if (status === "monitoring-with-scratch") {

            _label = "Scratch builds"

            $("#monitoring-icon").attr("class", "fa fa-fw fa-eye")

+         } else if (status === "monitoring-all") {

+           _label = "Monitoring all"

+           $("#monitoring-icon").attr("class", "fa fa-fw fa-eye")

+         } else if (status === "monitoring-all-scratch") {

+           _label = "All - Scratch builds"

+           $("#monitoring-icon").attr("class", "fa fa-fw fa-eye")

+         } else if (status === "monitoring-stable") {

+           _label = "Monitoring stable"

+           $("#monitoring-icon").attr("class", "fa fa-fw fa-eye")

+         } else if (status === "monitoring-stable-scratch") {

+           _label = "Stable - Scratch builds"

+           $("#monitoring-icon").attr("class", "fa fa-fw fa-eye")

          } else {

            $("#monitoring-icon").attr("class", "fa fa-fw fa-eye-slash")

          }
@@ -301,6 +353,16 @@ 

          } else if (selectedValue === "monitoring_and_scratch_option_button") {

              _status = "monitoring-with-scratch"

          }

+         if (selectedValue === "monitoring_all_option_button") {

+             _status = "monitoring-all";

+         } else if (selectedValue === "monitoring_all_and_scratch_option_button") {

+             _status = "monitoring-all-scratch"

+         }

+         if (selectedValue === "monitoring_stable_option_button") {

+             _status = "monitoring-stable";

+         } else if (selectedValue === "monitoring_stable_and_scratch_option_button") {

+             _status = "monitoring-stable-scratch"

+         }

  

          $.ajax({

            url: "{{ url_for('distgit_ns.anitya_patch_endpoint', repo=repo.name, namespace=repo.namespace) }}",
@@ -412,7 +474,7 @@ 

                  window.open(

                      "https://pagure.io/releng/new_issue?title="

                      + "Unretire {{repo.namespace}}/{{repo.name}}"

-                     + "&amp;template=package_unretiremet");

+                     + "&amp;template=package_unretirement");

              });

              _btn.prop( "title", "Package retired - Open a releng ticket to adopt it" );

              _btn.html("Retired");

file modified
+6 -2
@@ -1054,6 +1054,10 @@ 

              description="Creation of new project is not allowed on this \

                  pagure instance",

          )

+     

+     if pagure_config["PAGURE_AUTH"] == 'oidc' and flask.g.fas_user.can_create is False:

+         flask.abort(403,description="You are not allowed to create new projects on this instance")

+ 

  

      namespaces = pagure_config["ALLOWED_PREFIX"][:]

      if user:
@@ -1660,7 +1664,7 @@ 

      if admin_session_timedout():

          flask.flash("Action canceled, try it again", "error")

          return flask.redirect(

-             flask.url_for("auth_login", next=flask.request.url)

+             flask.url_for("auth_login", next=flask.request.url, _external=True)

          )

  

      # we just need an empty form here to validate that csrf token is present
@@ -1672,7 +1676,7 @@ 

          user.refuse_sessions_before = datetime.datetime.utcnow()

          flask.g.session.commit()

          flask.flash("All active sessions logged out")

-     return flask.redirect(flask.url_for("ui_ns.user_settings"))

+     return flask.redirect(flask.url_for("ui_ns.user_settings", _external=True))

  

  

  @UI_NS.route("/about")

file modified
+5
@@ -152,4 +152,9 @@ 

      except pagure.exceptions.PagureException as err:

          flask.flash(str(err), "error")

  

+     if flask.g.fas_user.get("ssh_key"):

+         del(flask.g.fas_user.ssh_key)

+     if flask.session.get("FLASK_FAS_OPENID_USER").get("ssh_key"):

+         del(flask.session["FLASK_FAS_OPENID_USER"]["ssh_key"])

+ 

      return flask.redirect(return_url)

file modified
+6 -3
@@ -23,11 +23,14 @@ 

  import arrow

  import bleach

  import flask

- import six

  import pygit2

+ import six

  

- from six.moves.urllib.parse import urlparse, parse_qsl

- from jinja2 import escape

+ try:

+     from jinja2 import escape

+ except ImportError:

+     from markupsafe import escape

+ from six.moves.urllib.parse import parse_qsl, urlparse

  

  import pagure.exceptions

  import pagure.lib.query

file modified
+3
@@ -355,6 +355,9 @@ 

      if not pagure_config.get("ENABLE_GROUP_MNGT", False):

          flask.abort(404)

  

+     if pagure_config["PAGURE_AUTH"] == 'oidc' and flask.g.fas_user.can_create is False:

+         flask.abort(403,description="You are not allowed to create new groups on this instance")

+ 

      user = pagure.lib.query.search_user(

          flask.g.session, username=flask.g.fas_user.username

      )

file modified
+9 -3
@@ -25,8 +25,11 @@ 

  import flask

  import pygit2

  import werkzeug.datastructures

- from sqlalchemy.exc import SQLAlchemyError

+ import werkzeug.security

+ 

  from binaryornot.helpers import is_binary_string

+ from six.moves.urllib.parse import urljoin

+ from sqlalchemy.exc import SQLAlchemyError

  

  import pagure.doc_utils

  import pagure.exceptions
@@ -1470,7 +1473,10 @@ 

      attachdir = os.path.join(

          pagure_config["ATTACHMENTS_FOLDER"], repo.fullname

      )

-     attachpath = os.path.join(attachdir, filename)

+ 

+     # sanitize path, filename must be inside attachdir to be valid

+     attachpath = werkzeug.security.safe_join(attachdir, filename)

+ 

      if not os.path.exists(attachpath):

          if not os.path.exists(attachdir):

              os.makedirs(attachdir)
@@ -1645,7 +1651,7 @@ 

          "ui_ns.view_issues", repo=repo, username=username, namespace=namespace

      )

      if pagure.utils.is_safe_url(flask.request.referrer):

-         return_point = flask.request.referrer

+         return_point = urljoin(flask.request.host_url, flask.request.referrer)

  

      form = pagure.forms.AddReportForm()

      if not form.validate_on_submit():

file modified
+2
@@ -96,6 +96,8 @@ 

      next_url = flask.request.form.get("next_url")

      if not next_url or next_url == "None":

          next_url = flask.url_for("ui_ns.index")

+     else:

+         next_url = urljoin(flask.request.host_url, next_url)

  

      if form.validate_on_submit():

          username = form.username.data

file modified
+11
@@ -59,6 +59,16 @@ 

                  ssh_key = b64decode(ssh_key).decode("ascii")

              except (TypeError, ValueError):

                  pass

+ 

+         oidc_group = pagure_config.get("RESTRICT_CREATE_BY_OIDC_GROUP", None)

+         oidc_group_count = pagure_config.get("RESTRICT_CREATE_BY_OIDC_GROUP_COUNT", 0)

+         can_create = True

+         if oidc_group:

+             if oidc_group not in info.get(groups_key, []):

+                 can_create = False

+             elif (oidc_group_count != 0) and (len(info.get(groups_key, [])) < oidc_group_count):

+                 can_create = False

+ 

          # Create the user object

          flask.g.fas_user = munch.Munch(

              username=username,
@@ -67,6 +77,7 @@ 

              ssh_key=ssh_key,

              groups=info.get(groups_key, []),

              login_time=flask.session["oidc_logintime"],

+             can_create=can_create,

          )

          flask.session["oidc_cached_userdata"] = dict(flask.g.fas_user)

  

file modified
+3 -2
@@ -25,6 +25,7 @@ 

  import os

  import re

  from math import ceil

+ from six.moves.urllib.parse import urljoin

  

  import flask

  import pygit2
@@ -2775,7 +2776,7 @@ 

      if flask.request.referrer is not None and pagure.utils.is_safe_url(

          flask.request.referrer

      ):

-         return_point = flask.request.referrer

+         return_point = urljoin(flask.request.host_url, flask.request.referrer)

  

      form = pagure.forms.ConfirmationForm()

      if not form.validate_on_submit():
@@ -2814,7 +2815,7 @@ 

  

      return_point = flask.url_for("ui_ns.index")

      if pagure.utils.is_safe_url(flask.request.referrer):

-         return_point = flask.request.referrer

+         return_point = urljoin(flask.request.host_url, flask.request.referrer)

  

      form = pagure.forms.ConfirmationForm()

      if not form.validate_on_submit():

file modified
+1 -1
@@ -10,7 +10,7 @@ 

  git remote rm proposed || true

  git gc --auto

  git remote add proposed "$REPO"

- GIT_TRACE=1 GIT_CURL_VERBOSE=1 git fetch proposed

+ GIT_TRACE=1 git fetch proposed

  git checkout origin/master

  git config --global user.email "you@example.com"

  git config --global user.name "Your Name"

file modified
+1 -1
@@ -53,7 +53,7 @@ 

          config = os.path.join(here, config)

      env["PAGURE_CONFIG"] = config

  

- cmd = [sys.executable, "-m", "celery", "worker", "-A", args.tasks]

+ cmd = [sys.executable, "-m", "celery", "-A", "worker", args.tasks]

  

  if args.queue:

      cmd.extend(["-Q", args.queue])

@@ -2291,6 +2291,7 @@ 

                  "delete_git_alias",

                  "fork_project",

                  "generate_acls_project",

+                 "group_modify",

                  "internal_access",

                  "issue_assign",

                  "issue_change_status",
@@ -2343,6 +2344,7 @@ 

                  "dummy_acls",

                  "fork_project",

                  "generate_acls_project",

+                 "group_modify",

                  "internal_access",

                  "issue_assign",

                  "issue_change_status",

@@ -1115,6 +1115,66 @@ 

          )

  

      @patch("pagure.lib.notify.send_email")

+     def test_api_pull_request_close_cross_project_token(self, send_email):

+         """ Test the api_pull_request_close method of the flask api for cross-project API token. """

+         send_email.return_value = True

+ 

+         tests.create_projects(self.session)

+         tests.create_tokens(self.session)

+         tests.create_tokens_acl(self.session)

+ 

+         # Create the pull-request to close

+         repo = pagure.lib.query.get_authorized_project(self.session, "test")

+         forked_repo = pagure.lib.query.get_authorized_project(

+             self.session, "test"

+         )

+         req = pagure.lib.query.new_pull_request(

+             session=self.session,

+             repo_from=forked_repo,

+             branch_from="master",

+             repo_to=repo,

+             branch_to="master",

+             title="test pull-request",

+             user="foo",

+         )

+         self.session.commit()

+         self.assertEqual(req.id, 1)

+         self.assertEqual(req.title, "test pull-request")

+         self.assertEqual(req.user.id, 2)

+ 

+         # Create a token for foo

+         item = pagure.lib.model.Token(

+             id="foobar_token",

+             user_id=2,

+             project_id=None,

+             expiration=datetime.datetime.utcnow()

+             + datetime.timedelta(days=30),

+         )

+         self.session.add(item)

+         self.session.commit()

+ 

+         # Allow the token to close PR

+         acls = pagure.lib.query.get_acls(self.session)

+         for acl in acls:

+             if acl.name == "pull_request_close":

+                 break

+         item = pagure.lib.model.TokenAcl(

+             token_id="foobar_token", acl_id=acl.id

+         )

+         self.session.add(item)

+         self.session.commit()

+ 

+         headers = {"Authorization": "token foobar_token"}

+ 

+         # User is the same that created this PR

+         output = self.app.post(

+             "/api/0/test/pull-request/1/close", headers=headers

+         )

+         self.assertEqual(output.status_code, 200)

+         data = json.loads(output.get_data(as_text=True))

+         self.assertDictEqual(data, {"message": "Pull-request closed!"})

+ 

+     @patch("pagure.lib.notify.send_email")

      def test_api_pull_request_close(self, send_email):

          """ Test the api_pull_request_close method of the flask api. """

          send_email.return_value = True
@@ -1224,6 +1284,123 @@ 

          self.assertDictEqual(data, {"message": "Pull-request closed!"})

  

      @patch("pagure.lib.notify.send_email")

+     def test_api_pull_request_reopen(self, send_email):

+         """Test the api_pull_request_reopen method of the flask api."""

+         send_email.return_value = True

+ 

+         tests.create_projects(self.session)

+         tests.create_tokens(self.session)

+         tests.create_tokens_acl(self.session)

+ 

+         # Create the pull-request to close and reopen

+         repo = pagure.lib.query.get_authorized_project(self.session, "test")

+         forked_repo = pagure.lib.query.get_authorized_project(

+             self.session, "test"

+         )

+         req = pagure.lib.query.new_pull_request(

+             session=self.session,

+             repo_from=forked_repo,

+             branch_from="master",

+             repo_to=repo,

+             branch_to="master",

+             title="test pull-request",

+             user="pingou",

+         )

+         self.session.commit()

+         self.assertEqual(req.id, 1)

+         self.assertEqual(req.title, "test pull-request")

+ 

+         headers = {"Authorization": "token aaabbbcccddd"}

+ 

+         # Invalid project

+         output = self.app.post(

+             "/api/0/foo/pull-request/1/close", headers=headers

+         )

+         self.assertEqual(output.status_code, 404)

+         data = json.loads(output.get_data(as_text=True))

+         self.assertDictEqual(

+             data, {"error": "Project not found", "error_code": "ENOPROJECT"}

+         )

+ 

+         # Valid token, wrong project

+         output = self.app.post(

+             "/api/0/test2/pull-request/1/close", headers=headers

+         )

+         self.assertEqual(output.status_code, 401)

+         data = json.loads(output.get_data(as_text=True))

+         self.assertEqual(

+             pagure.api.APIERROR.EINVALIDTOK.name, data["error_code"]

+         )

+         self.assertEqual(pagure.api.APIERROR.EINVALIDTOK.value, data["error"])

+ 

+         # Invalid PR

+         output = self.app.post(

+             "/api/0/test/pull-request/2/close", headers=headers

+         )

+         self.assertEqual(output.status_code, 404)

+         data = json.loads(output.get_data(as_text=True))

+         self.assertDictEqual(

+             data, {"error": "Pull-Request not found", "error_code": "ENOREQ"}

+         )

+ 

+         # Create a token for foo for this project

+         item = pagure.lib.model.Token(

+             id="foobar_token",

+             user_id=2,

+             project_id=1,

+             expiration=datetime.datetime.utcnow()

+             + datetime.timedelta(days=30),

+         )

+         self.session.add(item)

+         self.session.commit()

+ 

+         # Allow the token to close and reopen PR

+         acls = pagure.lib.query.get_acls(self.session)

+         for acl in acls:

+             if acl.name == "pull_request_close":

+                 break

+         item = pagure.lib.model.TokenAcl(

+             token_id="foobar_token", acl_id=acl.id

+         )

+         self.session.add(item)

+         self.session.commit()

+ 

+         headers = {"Authorization": "token foobar_token"}

+ 

+         # User not admin

+         output = self.app.post(

+             "/api/0/test/pull-request/1/close", headers=headers

+         )

+         self.assertEqual(output.status_code, 403)

+         data = json.loads(output.get_data(as_text=True))

+         self.assertDictEqual(

+             data,

+             {

+                 "error": "You are not allowed to merge/close pull-request "

+                 "for this project",

+                 "error_code": "ENOPRCLOSE",

+             },

+         )

+ 

+         headers = {"Authorization": "token aaabbbcccddd"}

+ 

+         # Close PR

+         output = self.app.post(

+             "/api/0/test/pull-request/1/close", headers=headers

+         )

+         self.assertEqual(output.status_code, 200)

+         data = json.loads(output.get_data(as_text=True))

+         self.assertDictEqual(data, {"message": "Pull-request closed!"})

+ 

+         # Reopen PR

+         output = self.app.post(

+             "/api/0/test/pull-request/1/reopen", headers=headers

+         )

+         self.assertEqual(output.status_code, 200)

+         data = json.loads(output.get_data(as_text=True))

+         self.assertDictEqual(data, {"message": "Pull-request reopened!"})

+ 

+     @patch("pagure.lib.notify.send_email")

      def test_api_pull_request_merge_pr_disabled(self, send_email):

          """Test the api_pull_request_merge method of the flask api when PR

          are disabled."""

@@ -764,5 +764,239 @@ 

          self.assertEqual(len(project.issues[0].related_prs), 1)

  

  

+ class PagureFlaskApiForkUpdateNoCommitterTests(tests.SimplePagureTest):

+     """Tests for the flask API of pagure for updating a PR

+     when the PR owner is not a committer in the target project

+     but he is the owner of the PR.

+     """

+ 

+     maxDiff = None

+ 

+     @patch("pagure.lib.git.update_git", MagicMock(return_value=True))

+     @patch("pagure.lib.notify.send_email", MagicMock(return_value=True))

+     def setUp(self):

+         """Set up the environnment, ran before every tests.

+ 

+         Pingou is the owner of the target project (test).

+         Maja has a fork of the test project and creates

+         a PR against Pingou parent project.

+         She should be able to update her own PR.

+         """

+         super(PagureFlaskApiForkUpdateNoCommitterTests, self).setUp()

+ 

+         tests.create_projects(self.session)

+         tests.add_content_git_repo(

+             os.path.join(self.path, "repos", "test.git")

+         )

+ 

+         # Fork

+         tests.create_user(self.session, "maja", "Maja M.", ["mm@f.com"])

+         project = pagure.lib.query.get_authorized_project(self.session, "test")

+         task = pagure.lib.query.fork_project(

+             session=self.session, user="maja", repo=project

+         )

+         self.session.commit()

+         self.assertEqual(

+             task.get(),

+             {

+                 "endpoint": "ui_ns.view_repo",

+                 "repo": "test",

+                 "namespace": None,

+                 "username": "maja",

+             },

+         )

+ 

+         tests.add_readme_git_repo(

+             os.path.join(self.path, "repos", "forks", "maja", "test.git")

+         )

+         project = pagure.lib.query.get_authorized_project(self.session, "test")

+         fork = pagure.lib.query.get_authorized_project(

+             self.session, "test", user="maja"

+         )

+ 

+         tests.create_tokens(self.session)

+         tests.create_tokens_acl(self.session)

+ 

+         req = pagure.lib.query.new_pull_request(

+             session=self.session,

+             repo_from=fork,

+             branch_from="master",

+             repo_to=project,

+             branch_to="master",

+             title="test pull-request",

+             user="maja",

+         )

+         self.session.commit()

+         self.assertEqual(req.id, 1)

+         self.assertEqual(req.title, "test pull-request")

+ 

+         # Assert the PR is open

+         self.session = pagure.lib.query.create_session(self.dbpath)

+         project = pagure.lib.query.get_authorized_project(self.session, "test")

+         self.assertEqual(len(project.requests), 1)

+         self.assertEqual(project.requests[0].status, "Open")

+         # Check how the PR renders in the API and the UI

+         output = self.app.get("/api/0/test/pull-request/1")

+         self.assertEqual(output.status_code, 200)

+         output = self.app.get("/test/pull-request/1")

+         self.assertEqual(output.status_code, 200)

+ 

+     def test_api_pull_request_updated_by_owner(self):

+         """Owners of PRs can update their own PRs."""

+ 

+         headers = {"Authorization": "token aaabbbcccddd"}

+ 

+         data = {

+             "title": "edited test PR",

+             "initial_comment": "Edited initial comment",

+         }

+ 

+         user = tests.FakeUser()

+         user.username = "maja"

+         with tests.user_set(self.app.application, user):

+             output = self.app.post(

+                 "/api/0/test/pull-request/1", data=data, headers=headers

+             )

+ 

+             self.assertEqual(output.status_code, 200)

+ 

+     def test_api_pull_request_not_updated_by_other_user(self):

+         """No repo committers or PR owners are allowed to update PR."""

+ 

+         headers = {"Authorization": "token aaabbbcccddd"}

+ 

+         data = {

+             "title": "edited test PR",

+             "initial_comment": "Edited initial comment",

+         }

+ 

+         tests.create_user(self.session, "other", "Another User", ["au@rh.com"])

+         user = tests.FakeUser()

+         user.username = "other"

+         with tests.user_set(self.app.application, user):

+             output = self.app.post(

+                 "/api/0/test/pull-request/1", data=data, headers=headers

+             )

+ 

+             self.assertEqual(output.status_code, 403)

+ 

+ 

+ class PagureFlaskApiForkUpdateNoCommitterTests(tests.SimplePagureTest):

+     """Tests for the flask API of pagure for updating a PR

+     when the PR owner is not a committer in the target project

+     but he is the owner of the PR.

+     """

+ 

+     maxDiff = None

+ 

+     @patch("pagure.lib.git.update_git", MagicMock(return_value=True))

+     @patch("pagure.lib.notify.send_email", MagicMock(return_value=True))

+     def setUp(self):

+         """Set up the environnment, ran before every tests.

+ 

+         Pingou is the owner of the target project (test).

+         Maja has a fork of the test project and creates

+         a PR against Pingou parent project.

+         She should be able to update her own PR.

+         """

+         super(PagureFlaskApiForkUpdateNoCommitterTests, self).setUp()

+ 

+         tests.create_projects(self.session)

+         tests.add_content_git_repo(

+             os.path.join(self.path, "repos", "test.git")

+         )

+ 

+         # Fork

+         tests.create_user(self.session, "maja", "Maja M.", ["mm@f.com"])

+         project = pagure.lib.query.get_authorized_project(self.session, "test")

+         task = pagure.lib.query.fork_project(

+             session=self.session, user="maja", repo=project

+         )

+         self.session.commit()

+         self.assertEqual(

+             task.get(),

+             {

+                 "endpoint": "ui_ns.view_repo",

+                 "repo": "test",

+                 "namespace": None,

+                 "username": "maja",

+             },

+         )

+ 

+         tests.add_readme_git_repo(

+             os.path.join(self.path, "repos", "forks", "maja", "test.git")

+         )

+         project = pagure.lib.query.get_authorized_project(self.session, "test")

+         fork = pagure.lib.query.get_authorized_project(

+             self.session, "test", user="maja"

+         )

+ 

+         tests.create_tokens(self.session)

+         tests.create_tokens_acl(self.session)

+ 

+         req = pagure.lib.query.new_pull_request(

+             session=self.session,

+             repo_from=fork,

+             branch_from="master",

+             repo_to=project,

+             branch_to="master",

+             title="test pull-request",

+             user="maja",

+         )

+         self.session.commit()

+         self.assertEqual(req.id, 1)

+         self.assertEqual(req.title, "test pull-request")

+ 

+         # Assert the PR is open

+         self.session = pagure.lib.query.create_session(self.dbpath)

+         project = pagure.lib.query.get_authorized_project(self.session, "test")

+         self.assertEqual(len(project.requests), 1)

+         self.assertEqual(project.requests[0].status, "Open")

+         # Check how the PR renders in the API and the UI

+         output = self.app.get("/api/0/test/pull-request/1")

+         self.assertEqual(output.status_code, 200)

+         output = self.app.get("/test/pull-request/1")

+         self.assertEqual(output.status_code, 200)

+ 

+     def test_api_pull_request_updated_by_owner(self):

+         """Owners of PRs can update their own PRs."""

+ 

+         headers = {"Authorization": "token aaabbbcccddd"}

+ 

+         data = {

+             "title": "edited test PR",

+             "initial_comment": "Edited initial comment",

+         }

+ 

+         user = tests.FakeUser()

+         user.username = "maja"

+         with tests.user_set(self.app.application, user):

+             output = self.app.post(

+                 "/api/0/test/pull-request/1", data=data, headers=headers

+             )

+ 

+             self.assertEqual(output.status_code, 200)

+ 

+     def test_api_pull_request_not_updated_by_other_user(self):

+         """No repo committers or PR owners are allowed to update PR."""

+ 

+         headers = {"Authorization": "token aaabbbcccddd"}

+ 

+         data = {

+             "title": "edited test PR",

+             "initial_comment": "Edited initial comment",

+         }

+ 

+         tests.create_user(self.session, "other", "Another User", ["au@rh.com"])

+         user = tests.FakeUser()

+         user.username = "other"

+         with tests.user_set(self.app.application, user):

+             output = self.app.post(

+                 "/api/0/test/pull-request/1", data=data, headers=headers

+             )

+ 

+             self.assertEqual(output.status_code, 403)

+ 

+ 

  if __name__ == "__main__":

      unittest.main(verbosity=2)

@@ -15,6 +15,8 @@ 

  import sys

  import os

  import json

+ from unittest.mock import patch

+ from sqlalchemy.exc import SQLAlchemyError

  

  sys.path.insert(

      0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")
@@ -22,6 +24,7 @@ 

  

  import pagure.api

  import pagure.lib.query

+ from pagure.exceptions import PagureException

  import tests

  

  
@@ -773,6 +776,388 @@ 

          # a different order that was the order of requests

          assert projects == ["test", "test2"]

  

+     def test_api_group_add_member_authenticated(self):

+         """

+         Test the api_group_add_member method of the flask api with an

+         authenticated user.

+         """

+         tests.create_tokens(self.session)

+         tests.create_tokens_acl(self.session)

+ 

+         headers = {"Authorization": "token aaabbbcccddd"}

+         payload = {"user": "foo"}

+         output = self.app.post(

+             "/api/0/group/some_group/add", data=payload, headers=headers

+         )

+         self.assertEqual(output.status_code, 200)

+         exp = {

+             "display_name": "Some Group",

+             "full_url": "http://localhost.localdomain/group/some_group",

+             "description": None,

+             "creator": {

+                 "fullname": "PY C",

+                 "full_url": "http://localhost.localdomain/user/pingou",

+                 "url_path": "user/pingou",

+                 "default_email": "bar@pingou.com",

+                 "emails": ["bar@pingou.com", "foo@pingou.com"],

+                 "name": "pingou",

+             },

+             "members": ["pingou", "foo"],

+             "date_created": "1492020239",

+             "group_type": "user",

+             "name": "some_group",

+         }

+         data = json.loads(output.get_data(as_text=True))

+         data["date_created"] = "1492020239"

+         self.assertDictEqual(data, exp)

+ 

+     def test_api_group_add_member_unauthenticated(self):

+         """

+         Assert that api_group_add_member method will fail with

+         unauthenticated user.

+         """

+         headers = {"Authorization": "token aaabbbcccddd"}

+         payload = {"user": "foo"}

+         output = self.app.post(

+             "/api/0/group/some_group/add", data=payload, headers=headers

+         )

+         self.assertEqual(output.status_code, 401)

+         exp = {

+             "error": (

+                 "Invalid or expired token. "

+                 "Please visit "

+                 "http://localhost.localdomain/settings#nav-api-tab "

+                 "to get or renew your API token."

+             ),

+             "error_code": "EINVALIDTOK",

+             "errors": "Invalid token",

+         }

+         data = json.loads(output.get_data(as_text=True))

+         self.assertDictEqual(data, exp)

+ 

+     def test_api_group_add_member_no_permission(self):

+         """

+         Assert that api_group_add_member method will fail with

+         user that don't have permissions to add member to group.

+         """

+         # Create tokens for foo user

+         tests.create_tokens(self.session, user_id=2)

+         tests.create_tokens_acl(self.session)

+         headers = {"Authorization": "token aaabbbcccddd"}

+         payload = {"user": "foo"}

+         output = self.app.post(

+             "/api/0/group/some_group/add", data=payload, headers=headers

+         )

+         self.assertEqual(output.status_code, 400)

+         exp = {

+             "error": (

+                 "An error occurred at the database level "

+                 "and prevent the action from reaching completion"

+             ),

+             "error_code": "EDBERROR",

+             "errors": ["You are not allowed to add user to this group"],

+         }

+         data = json.loads(output.get_data(as_text=True))

+         self.assertDictEqual(data, exp)

+ 

+     def test_api_group_add_member_no_group(self):

+         """

+         Assert that api_group_add_member method will fail when group doesn't

+         exist.

+         """

+         tests.create_tokens(self.session)

+         tests.create_tokens_acl(self.session)

+ 

+         headers = {"Authorization": "token aaabbbcccddd"}

+         payload = {"user": "foo"}

+         output = self.app.post(

+             "/api/0/group/no_group/add", data=payload, headers=headers

+         )

+         self.assertEqual(output.status_code, 404)

+         exp = {"error": "Group not found", "error_code": "ENOGROUP"}

+         data = json.loads(output.get_data(as_text=True))

+         self.assertDictEqual(data, exp)

+ 

+     def test_api_group_add_member_invalid_request(self):

+         """

+         Assert that api_group_add_member method will fail when request

+         is invalid.

+         """

+         tests.create_tokens(self.session)

+         tests.create_tokens_acl(self.session)

+ 

+         headers = {"Authorization": "token aaabbbcccddd"}

+         payload = {"dummy": "foo"}

+         output = self.app.post(

+             "/api/0/group/some_group/add", data=payload, headers=headers

+         )

+         self.assertEqual(output.status_code, 400)

+         exp = {

+             "error": "Invalid or incomplete input submitted",

+             "error_code": "EINVALIDREQ",

+             "errors": {"user": ["This field is required."]},

+         }

+         data = json.loads(output.get_data(as_text=True))

+         self.assertDictEqual(data, exp)

+ 

+     @patch("pagure.lib.query.add_user_to_group")

+     def test_api_group_add_member_pagure_error(self, mock_add_user):

+         """

+         Assert that api_group_add_member method will fail when pagure

+         throws exception.

+         """

+         tests.create_tokens(self.session)

+         tests.create_tokens_acl(self.session)

+         mock_add_user.side_effect = PagureException("Error")

+ 

+         headers = {"Authorization": "token aaabbbcccddd"}

+         payload = {"user": "foo"}

+         output = self.app.post(

+             "/api/0/group/some_group/add", data=payload, headers=headers

+         )

+         self.assertEqual(output.status_code, 400)

+         exp = {

+             "error": (

+                 "An error occurred at the database level "

+                 "and prevent the action from reaching completion"

+             ),

+             "error_code": "EDBERROR",

+             "errors": ["Error"],

+         }

+         data = json.loads(output.get_data(as_text=True))

+         self.assertDictEqual(data, exp)

+ 

+     @patch("pagure.lib.query.add_user_to_group")

+     def test_api_group_add_member_sqlalchemy_error(self, mock_add_user):

+         """

+         Assert that api_group_add_member method will fail when SQLAlchemy

+         throws exception.

+         """

+         tests.create_tokens(self.session)

+         tests.create_tokens_acl(self.session)

+         mock_add_user.side_effect = SQLAlchemyError("Error")

+ 

+         headers = {"Authorization": "token aaabbbcccddd"}

+         payload = {"user": "foo"}

+         output = self.app.post(

+             "/api/0/group/some_group/add", data=payload, headers=headers

+         )

+         self.assertEqual(output.status_code, 400)

+         exp = {

+             "error": (

+                 "An error occurred at the database level "

+                 "and prevent the action from reaching completion"

+             ),

+             "error_code": "EDBERROR",

+             "errors": ["Error"],

+         }

+         data = json.loads(output.get_data(as_text=True))

+         self.assertDictEqual(data, exp)

+ 

+     def test_api_group_remove_member_authenticated(self):

+         """

+         Test the api_group_remove_member method of the flask api with an

+         authenticated user.

+         """

+         tests.create_tokens(self.session)

+         tests.create_tokens_acl(self.session)

+ 

+         headers = {"Authorization": "token aaabbbcccddd"}

+         payload = {"user": "foo"}

+         # Add user first

+         output = self.app.post(

+             "/api/0/group/some_group/add", data=payload, headers=headers

+         )

+         exp = {

+             "display_name": "Some Group",

+             "full_url": "http://localhost.localdomain/group/some_group",

+             "description": None,

+             "creator": {

+                 "fullname": "PY C",

+                 "full_url": "http://localhost.localdomain/user/pingou",

+                 "url_path": "user/pingou",

+                 "default_email": "bar@pingou.com",

+                 "emails": ["bar@pingou.com", "foo@pingou.com"],

+                 "name": "pingou",

+             },

+             "members": ["pingou", "foo"],

+             "date_created": "1492020239",

+             "group_type": "user",

+             "name": "some_group",

+         }

+         data = json.loads(output.get_data(as_text=True))

+         data["date_created"] = "1492020239"

+         self.assertDictEqual(data, exp)

+ 

+         # Then remove it

+         output = self.app.post(

+             "/api/0/group/some_group/remove", data=payload, headers=headers

+         )

+         self.assertEqual(output.status_code, 200)

+         exp = {

+             "display_name": "Some Group",

+             "full_url": "http://localhost.localdomain/group/some_group",

+             "description": None,

+             "creator": {

+                 "fullname": "PY C",

+                 "full_url": "http://localhost.localdomain/user/pingou",

+                 "url_path": "user/pingou",

+                 "default_email": "bar@pingou.com",

+                 "emails": ["bar@pingou.com", "foo@pingou.com"],

+                 "name": "pingou",

+             },

+             "members": ["pingou"],

+             "date_created": "1492020239",

+             "group_type": "user",

+             "name": "some_group",

+         }

+         data = json.loads(output.get_data(as_text=True))

+         data["date_created"] = "1492020239"

+         self.assertDictEqual(data, exp)

+ 

+     def test_api_group_remove_member_unauthenticated(self):

+         """

+         Assert that api_group_remove_member method will fail with

+         unauthenticated user.

+         """

+         headers = {"Authorization": "token aaabbbcccddd"}

+         payload = {"user": "foo"}

+         output = self.app.post(

+             "/api/0/group/some_group/remove", data=payload, headers=headers

+         )

+         self.assertEqual(output.status_code, 401)

+         exp = {

+             "error": (

+                 "Invalid or expired token. "

+                 "Please visit "

+                 "http://localhost.localdomain/settings#nav-api-tab "

+                 "to get or renew your API token."

+             ),

+             "error_code": "EINVALIDTOK",

+             "errors": "Invalid token",

+         }

+         data = json.loads(output.get_data(as_text=True))

+         self.assertDictEqual(data, exp)

+ 

+     def test_api_group_remove_member_no_permission(self):

+         """

+         Assert that api_group_remove_member method will fail with

+         user that don't have permissions to remove member to group.

+         """

+         # Create tokens for foo user

+         tests.create_tokens(self.session, user_id=2)

+         tests.create_tokens_acl(self.session)

+         headers = {"Authorization": "token aaabbbcccddd"}

+         payload = {"user": "foo"}

+         output = self.app.post(

+             "/api/0/group/some_group/remove", data=payload, headers=headers

+         )

+         self.assertEqual(output.status_code, 400)

+         exp = {

+             "error": (

+                 "An error occurred at the database level "

+                 "and prevent the action from reaching completion"

+             ),

+             "error_code": "EDBERROR",

+             "errors": ["You are not allowed to remove user from this group"],

+         }

+         data = json.loads(output.get_data(as_text=True))

+         self.assertDictEqual(data, exp)

+ 

+     def test_api_group_remove_member_no_group(self):

+         """

+         Assert that api_group_remove_member method will fail when group doesn't

+         exist.

+         """

+         tests.create_tokens(self.session)

+         tests.create_tokens_acl(self.session)

+ 

+         headers = {"Authorization": "token aaabbbcccddd"}

+         payload = {"user": "foo"}

+         output = self.app.post(

+             "/api/0/group/no_group/remove", data=payload, headers=headers

+         )

+         self.assertEqual(output.status_code, 404)

+         exp = {"error": "Group not found", "error_code": "ENOGROUP"}

+         data = json.loads(output.get_data(as_text=True))

+         self.assertDictEqual(data, exp)

+ 

+     def test_api_group_remove_member_invalid_request(self):

+         """

+         Assert that api_group_remove_member method will fail when request

+         is invalid.

+         """

+         tests.create_tokens(self.session)

+         tests.create_tokens_acl(self.session)

+ 

+         headers = {"Authorization": "token aaabbbcccddd"}

+         payload = {"dummy": "foo"}

+         output = self.app.post(

+             "/api/0/group/some_group/remove", data=payload, headers=headers

+         )

+         self.assertEqual(output.status_code, 400)

+         exp = {

+             "error": "Invalid or incomplete input submitted",

+             "error_code": "EINVALIDREQ",

+             "errors": {"user": ["This field is required."]},

+         }

+         data = json.loads(output.get_data(as_text=True))

+         self.assertDictEqual(data, exp)

+ 

+     @patch("pagure.lib.query.delete_user_of_group")

+     def test_api_group_remove_member_pagure_error(self, mock_remove_user):

+         """

+         Assert that api_group_remove_member method will fail when pagure

+         throws exception.

+         """

+         tests.create_tokens(self.session)

+         tests.create_tokens_acl(self.session)

+         mock_remove_user.side_effect = PagureException("Error")

+ 

+         headers = {"Authorization": "token aaabbbcccddd"}

+         payload = {"user": "foo"}

+         output = self.app.post(

+             "/api/0/group/some_group/remove", data=payload, headers=headers

+         )

+         self.assertEqual(output.status_code, 400)

+         exp = {

+             "error": (

+                 "An error occurred at the database level "

+                 "and prevent the action from reaching completion"

+             ),

+             "error_code": "EDBERROR",

+             "errors": ["Error"],

+         }

+         data = json.loads(output.get_data(as_text=True))

+         self.assertDictEqual(data, exp)

+ 

+     @patch("pagure.lib.query.delete_user_of_group")

+     def test_api_group_remove_member_sqlalchemy_error(self, mock_remove_user):

+         """

+         Assert that api_group_remove_member method will fail when SQLAlchemy

+         throws exception.

+         """

+         tests.create_tokens(self.session)

+         tests.create_tokens_acl(self.session)

+         mock_remove_user.side_effect = SQLAlchemyError("Error")

+ 

+         headers = {"Authorization": "token aaabbbcccddd"}

+         payload = {"user": "foo"}

+         output = self.app.post(

+             "/api/0/group/some_group/remove", data=payload, headers=headers

+         )

+         self.assertEqual(output.status_code, 400)

+         exp = {

+             "error": (

+                 "An error occurred at the database level "

+                 "and prevent the action from reaching completion"

+             ),

+             "error_code": "EDBERROR",

+             "errors": ["Error"],

+         }

+         data = json.loads(output.get_data(as_text=True))

+         self.assertDictEqual(data, exp)

+ 

  

  if __name__ == "__main__":

      unittest.main(verbosity=2)

@@ -1214,7 +1214,7 @@ 

          )

          self.assertEqual(output.status_code, 200)

          data = json.loads(output.get_data(as_text=True))

-         self.assertEqual(len(data["requests"]), 0)

+         self.assertEqual(len(data["requests"]), 6)

  

          yesterday = today - datetime.timedelta(days=1)

          output = self.app.get(
@@ -1223,7 +1223,7 @@ 

          )

          self.assertEqual(output.status_code, 200)

          data = json.loads(output.get_data(as_text=True))

-         self.assertEqual(len(data["requests"]), 0)

+         self.assertEqual(len(data["requests"]), 6)

  

          tomorrow = today + datetime.timedelta(days=1)

          output = self.app.get(
@@ -1232,7 +1232,7 @@ 

          )

          self.assertEqual(output.status_code, 200)

          data = json.loads(output.get_data(as_text=True))

-         self.assertEqual(len(data["requests"]), 6)

+         self.assertEqual(len(data["requests"]), 0)

  

      @patch("pagure.lib.notify.send_email")

      def test_api_view_user_requests_filed_closed(self, mockemail):
@@ -1598,7 +1598,7 @@ 

          )

          self.assertEqual(output.status_code, 200)

          data = json.loads(output.get_data(as_text=True))

-         self.assertEqual(len(data["requests"]), 0)

+         self.assertEqual(len(data["requests"]), 6)

  

          yesterday = today - datetime.timedelta(days=1)

          output = self.app.get(
@@ -1607,7 +1607,7 @@ 

          )

          self.assertEqual(output.status_code, 200)

          data = json.loads(output.get_data(as_text=True))

-         self.assertEqual(len(data["requests"]), 0)

+         self.assertEqual(len(data["requests"]), 6)

  

          tomorrow = today + datetime.timedelta(days=1)

          output = self.app.get(
@@ -1616,7 +1616,7 @@ 

          )

          self.assertEqual(output.status_code, 200)

          data = json.loads(output.get_data(as_text=True))

-         self.assertEqual(len(data["requests"]), 6)

+         self.assertEqual(len(data["requests"]), 0)

  

      @patch("pagure.lib.notify.send_email")

      def test_api_view_user_requests_actionable_closed(self, mockemail):

@@ -18,6 +18,7 @@ 

  

  import json

  import pagure_messages

+ import pygit2

  from fedora_messaging import api, testing

  from mock import ANY, patch, MagicMock

  
@@ -249,7 +250,12 @@ 

              output = self.app.get("/test/pull-request/1")

              self.assertEqual(output.status_code, 200)

              output_text = output.get_data(as_text=True)

-             self.assertIn("rebased onto", output_text)

+             orig_repo_obj = pygit2.Repository(

+                 os.path.join(self.path, "repos", "test.git")

+             )

+             orig_commit = orig_repo_obj.lookup_branch("master").peel().hex

+             expected = f'rebased onto <a href="/test/c/{orig_commit}"'

+             self.assertIn(expected, output_text)

              repo = pagure.lib.query._get_project(self.session, "test")

              self.assertEqual(repo.requests[0].comments[0].user.username, "bar")

  

@@ -13,7 +13,7 @@ 

  import unittest

  import sys

  import os

- import time

+ from zipfile import ZipFile

  

  import mock

  import pygit2
@@ -123,6 +123,78 @@ 

  

          self.assertEqual(os.listdir(self.archive_path), [])

  

+     # All tests against test3 repo complete, re-use repo for symlink test

+     def test_project_with_symlink_zip(self):

+         """Test that symlink rather than target file gets added to zip"""

+         filename = "os-release"

+         symlinkfile_target = "/etc/os-release"

+         directory = "etc"

+         symlinkdir_target = "/etc"

+         readme = "README.rst"

+         archivename = "test3.zip"

+         namespace = "somenamespace"

+         reponame = "test3"

+         repopath = os.path.join(

+             self.path, "repos", namespace, "%s.git" % reponame

+         )

+         repo = pygit2.Repository(repopath)

+         archivepath = os.path.join(self.archive_path, namespace, reponame)

+         tests.add_commit_git_repo(

+             repopath,

+             ncommits=1,

+             filename=filename,

+             symlink_to=symlinkfile_target,

+         )

+         tests.add_commit_git_repo(

+             repopath,

+             ncommits=1,

+             filename=directory,

+             symlink_to=symlinkdir_target,

+         )

+         tests.add_readme_git_repo(repopath)

+         commit = repo.head.target.hex

+ 

+         with mock.patch.dict(

+             "pagure.config.config",

+             {"ARCHIVE_FOLDER": os.path.join(self.path, "archives")},

+         ):

+             output = self.app.get(

+                 "/%s/%s/archive/%s/%s"

+                 % (namespace, reponame, commit, archivename),

+                 follow_redirects=True,

+             )

+ 

+             self.assertEqual(output.status_code, 200)

+ 

+         # Was a subfolder for the correct commit created?

+         self.assertEqual(os.listdir(archivepath), [commit])

+ 

+         archivepath_commit = os.path.join(archivepath, commit)

+         # Does the subfolder contain the expected zip file?

+         self.assertEqual(os.listdir(archivepath_commit), [archivename])

+ 

+         archivepath_zip = os.path.join(archivepath_commit, archivename)

+         # Is the test file in the zip archive a symlink?

+         with ZipFile(os.path.join(archivepath_zip)) as testzip:

+             self.assertIn(

+                 "lrw-r--r--",

+                 str(testzip.getinfo("%s/%s" % (reponame, filename))),

+             )

+ 

+         # Is the test directory in the zip archive a symlink?

+         with ZipFile(os.path.join(archivepath_zip)) as testzip:

+             self.assertIn(

+                 "?rwxr-xr-x",

+                 str(testzip.getinfo("%s/%s/" % (reponame, directory))),

+             )

+ 

+         # Is the readme still an actual file in the zip archive?

+         with ZipFile(os.path.join(archivepath_zip)) as testzip:

+             self.assertIn(

+                 "-rw-r--r--",

+                 str(testzip.getinfo("%s/%s" % (reponame, readme))),

+             )

+ 

      def test_project_no_tag(self):

          """Test getting the archive of a non-empty project but without

          tags."""

@@ -1316,7 +1316,7 @@ 

          )

          self.assertIn(

              '<a href="/login/?next=http%3A%2F%2Flocalhost%2Ftest%2Fissue%2F1">'

-             "Login</a>\n          to comment on this ticket.",

+             "Log in</a>\n          to comment on this ticket.",

              output_text,

          )

  
@@ -1359,7 +1359,7 @@ 

          )

          self.assertIn(

              '<a href="/login/?next=http%3A%2F%2Flocalhost%2Ftest%2Fissue%2F1">'

-             "Login</a>\n          to comment on this ticket.",

+             "Log in</a>\n          to comment on this ticket.",

              output_text,

          )

  
@@ -1380,7 +1380,7 @@ 

                  output_text,

              )

              self.assertFalse(

-                 '<a href="/login/">Login</a> to comment on this ticket.'

+                 '<a href="/login/">Log in</a> to comment on this ticket.'

                  in output_text

              )

              # Not author nor admin = No take
@@ -1503,7 +1503,7 @@ 

          )

          self.assertIn(

              '<a href="/login/?next=http%3A%2F%2Flocalhost%2Ftest%2Fissue%2F1">'

-             "Login</a>\n          to comment on this ticket.",

+             "Log in</a>\n          to comment on this ticket.",

              output_text,

          )

  
@@ -1530,7 +1530,7 @@ 

                  output_text,

              )

              self.assertFalse(

-                 '<a href="/login/">Login</a> to comment on this ticket.'

+                 '<a href="/login/">Log in</a> to comment on this ticket.'

                  in output_text

              )

              # author admin = take
@@ -1577,7 +1577,7 @@ 

          )

          self.assertTrue(

              '<a href="/login/?next=http%3A%2F%2Flocalhost%2Ftest%2Fissue%2F1">'

-             "Login</a>\n          to comment on this ticket." in output_text

+             "Log in</a>\n          to comment on this ticket." in output_text

          )

  

          # Create issues to play with
@@ -1607,7 +1607,7 @@ 

                  output_text,

              )

              self.assertFalse(

-                 '<a href="/login/">Login</a> to comment on this ticket.'

+                 '<a href="/login/">Log in</a> to comment on this ticket.'

                  in output_text

              )

              # user has ticket = take ok
@@ -1718,7 +1718,7 @@ 

                  output_text,

              )

              self.assertNotIn(

-                 '<a href="/login/">Login</a> to comment on this ticket.',

+                 '<a href="/login/">Log in</a> to comment on this ticket.',

                  output_text,

              )

              # user has ticket = take ok

@@ -110,7 +110,7 @@ 

          )

          self.assertTrue(

              '<a href="/login/?next=http%3A%2F%2Flocalhost%2Ftest%2Fissue%2F1">'

-             "Login</a>\n          to comment on this ticket."

+             "Log in</a>\n          to comment on this ticket."

              in output.get_data(as_text=True)

          )

  
@@ -139,7 +139,7 @@ 

                  output_text,

              )

              self.assertNotIn(

-                 '<a href="/login/">Login</a> to comment on this ticket.',

+                 '<a href="/login/">Log in</a> to comment on this ticket.',

                  output_text,

              )

  
@@ -203,7 +203,7 @@ 

                  output_text,

              )

              self.assertNotIn(

-                 '<a href="/login/">Login</a> to comment on this ticket.',

+                 '<a href="/login/">Log in</a> to comment on this ticket.',

                  output_text,

              )

  
@@ -373,7 +373,7 @@ 

          )

          self.assertIn(

              '<a href="/login/?next=http%3A%2F%2Flocalhost%2Ftest%2Fissue%2F1">'

-             "Login</a>\n          to comment on this ticket.",

+             "Log in</a>\n          to comment on this ticket.",

              output_text,

          )

  
@@ -402,7 +402,7 @@ 

                  output_text,

              )

              self.assertNotIn(

-                 '<a href="/login/">Login</a> to comment on this ticket.',

+                 '<a href="/login/">Log in</a> to comment on this ticket.',

                  output_text,

              )

  
@@ -628,7 +628,7 @@ 

          )

          self.assertTrue(

              '<a href="/login/?next=http%3A%2F%2Flocalhost%2Ftest%2Fissue%2F1">'

-             "Login</a>\n            to comment on this ticket.",

+             "Log in</a>\n            to comment on this ticket.",

              output_text,

          )

  
@@ -657,7 +657,7 @@ 

                  output_text,

              )

              self.assertNotIn(

-                 '<a href="/login/">Login</a> to comment on this ticket.',

+                 '<a href="/login/">Log in</a> to comment on this ticket.',

                  output_text,

              )

  
@@ -881,7 +881,7 @@ 

          )

          self.assertTrue(

              '<a href="/login/?next=http%3A%2F%2Flocalhost%2Ftest%2Fissue%2F1">'

-             "Login</a>\n          to comment on this ticket."

+             "Log in</a>\n          to comment on this ticket."

              in output.get_data(as_text=True)

          )

  
@@ -910,7 +910,7 @@ 

                  output_text,

              )

              self.assertNotIn(

-                 '<a href="/login/">Login</a> to comment on this ticket.',

+                 '<a href="/login/">Log in</a> to comment on this ticket.',

                  output_text,

              )

  

@@ -214,7 +214,7 @@ 

          )

          self.assertIn(

              '<a href="/login/?next=http%3A%2F%2Flocalhost%2Ftest%2Fissue%2F1">'

-             "Login</a>\n          to comment on this ticket.",

+             "Log in</a>\n          to comment on this ticket.",

              output_text,

          )

  
@@ -235,7 +235,7 @@ 

                  output_text,

              )

              self.assertFalse(

-                 '<a href="/login/">Login</a> to comment on this ticket.'

+                 '<a href="/login/">Log in</a> to comment on this ticket.'

                  in output_text

              )

              # Not author nor admin but open_access = take
@@ -314,7 +314,7 @@ 

          )

          self.assertTrue(

              '<a href="/login/?next=http%3A%2F%2Flocalhost%2Ftest%2Fissue%2F1">'

-             "Login</a>\n          to comment on this ticket." in output_text

+             "Log in</a>\n          to comment on this ticket." in output_text

          )

  

          # Create issues to play with
@@ -344,7 +344,7 @@ 

                  output_text,

              )

              self.assertFalse(

-                 '<a href="/login/">Login</a> to comment on this ticket.'

+                 '<a href="/login/">Log in</a> to comment on this ticket.'

                  in output_text

              )

              # user has ticket = take ok
@@ -446,7 +446,7 @@ 

                  output_text,

              )

              self.assertNotIn(

-                 '<a href="/login/">Login</a> to comment on this ticket.',

+                 '<a href="/login/">Log in</a> to comment on this ticket.',

                  output_text,

              )

              # user has ticket = take ok

@@ -297,8 +297,8 @@ 

          self.assertEqual(item.user, "foouser")

          self.assertEqual(item.token, None)

  

-         # Login but cannot save the session to the DB due to the missing IP

-         # address in the flask request

+         # Login works but cannot save the session to the DB due to the missing

+         # IP address in the flask request

          data["password"] = "barpass"

          output = self.app.post("/dologin", data=data, follow_redirects=True)

          self.assertEqual(output.status_code, 200)
@@ -474,6 +474,10 @@ 

              '<span class="d-none d-md-inline">Settings</span>', output_text

          )

  

+         output = self.app.get("/login/?next=%2f%2f%09%2fgoogle.fr")

+         self.assertEqual(output.status_code, 302)

+         self.assertEqual(output.location, "http://localhost/google.fr")

+ 

      @patch.dict("pagure.config.config", {"PAGURE_AUTH": "local"})

      @patch.dict("pagure.config.config", {"CHECK_SESSION_IP": False})

      def test_has_settings(self):
@@ -518,7 +522,7 @@ 

      @patch.dict("pagure.config.config", {"PAGURE_AUTH": "local"})

      @patch("pagure.lib.notify.send_email", MagicMock(return_value=True))

      def test_non_ascii_password(self):

-         """Test login and create user functionality when the password is

+         """Test login and user creation functionality when the password is

          non-ascii.

          """

  
@@ -1068,6 +1072,14 @@ 

                  output.get_data(as_text=True),

              )

  

+         user = tests.FakeUser(username="foo")

+         with tests.user_set(self.app.application, user):

+             output = self.app.get("/logout/?next=%2f%2f%09%2fgoogle.fr")

+             self.assertEqual(output.status_code, 302)

+             self.assertTrue(

+                 output.headers["location"] in ("http://localhost/google.fr",)

+             )

+ 

      @patch.dict("pagure.config.config", {"PAGURE_AUTH": "local"})

      def test_settings_admin_session_timedout(self):

          """ Test the admin_session_timedout with settings endpoint. """
@@ -1085,7 +1097,14 @@ 

              # redirect again for the login page

              output = self.app.get("/settings/")

              self.assertEqual(output.status_code, 302)

-             self.assertIn("http://localhost/login/", output.location)

+             self.assertTrue(

+                 output.location

+                 in (

+                     "http://localhost/login/",

+                     "/login/?next=http%3A%2F%2Flocalhost%2Fsettings%2F",

+                     "http://localhost/login/?next=http%3A%2F%2Flocalhost%2Fsettings%2F",

+                 )

+             )

          # session did not expire

          user.login_time = datetime.datetime.utcnow() - lifetime + td1

          with tests.user_set(self.app.application, user):
@@ -1127,14 +1146,17 @@ 

              data = {"csrf_token": self.get_csrf()}

              output = self.app.post("/settings/forcelogout/", data=data)

              self.assertEqual(output.status_code, 302)

-             self.assertEqual(

-                 output.headers["Location"], "http://localhost/settings"

+             self.assertTrue(

+                 output.headers["Location"]

+                 in ("http://localhost/settings", "/settings")

              )

  

              # We should now get redirected to index, because our session became

              # invalid

              output = self.app.get("/settings")

-             self.assertEqual(output.headers["Location"], "http://localhost/")

+             self.assertTrue(

+                 output.headers["Location"] in ("http://localhost/", "/")

+             )

  

              # After changing the login_time to now, the session should again be

              # valid

file modified
+31 -3
@@ -29,6 +29,8 @@ 

  import pagure.lib.model

  import tests

  

+ from pagure.config import config as pagure_config

+ 

  

  class PagureLibtests_search_user(tests.Modeltests):

      """
@@ -5243,10 +5245,16 @@ 

              html = pagure.lib.query.text2markdown(text)

              self.assertEqual(html, expected)

  

-     def test_set_redis(self):

-         """ Test the set_redis function of pagure.lib.query. """

+     def test_set_redis_tcpip(self):

+         """Test the set_redis function of pagure.lib.query using address/port."""

          self.assertIsNone(pagure.lib.query.REDIS)

-         pagure.lib.query.set_redis("0.0.0.0", 6379, 0)

+         pagure.lib.query.set_redis("0.0.0.0", 6379)

+         self.assertIsNotNone(pagure.lib.query.REDIS)

+ 

+     def test_set_redis_unix(self):

+         """Test the set_redis function of pagure.lib.query using a Unix socket."""

+         self.assertIsNone(pagure.lib.query.REDIS)

+         pagure.lib.query.set_redis(socket="/run/redis/pagure.sock")

          self.assertIsNotNone(pagure.lib.query.REDIS)

  

      def test_set_pagure_ci(self):
@@ -5667,6 +5675,7 @@ 

                  "delete_git_alias",

                  "fork_project",

                  "generate_acls_project",

+                 "group_modify",

                  "internal_access",

                  "issue_assign",

                  "issue_change_status",
@@ -5708,6 +5717,25 @@ 

              [a.name for a in acls], ["create_project", "issue_create"]

          )

  

+     def test_get_acls_restrict_CROSS_PROJECT_ACLS(self):

+         """Test CROSS_PROJECT_ACLS is well formed and working with get_acls."""

+         acls = pagure.lib.query.get_acls(

+             self.session, restrict=pagure_config.get("CROSS_PROJECT_ACLS")

+         )

+         self.assertEqual(

+             sorted([a.name for a in acls]),

+             [

+                 "commit",

+                 "create_project",

+                 "fork_project",

+                 "group_modify",

+                 "modify_project",

+                 "pull_request_create",

+                 "pull_request_update",

+                 "update_watch_status",

+             ],

+         )

+ 

      def test_filter_img_src(self):

          """ Test the filter_img_src function of pagure.lib.query. """

          for name in ("alt", "height", "width", "class"):

@@ -3210,6 +3210,53 @@ 

              output = pagure.lib.git.get_author_email(githash, gitrepo)

              self.assertEqual(output, "pagure")

  

+     def test_get_changed_files(self):

+         """Test the get_changed_files method of pagure.lib.git."""

+ 

+         self.test_update_git()

+ 

+         repo = pagure.lib.query.get_authorized_project(

+             self.session, "test_ticket_repo"

+         )

+         issue = pagure.lib.query.search_issues(self.session, repo, issueid=1)

+ 

+         gitrepo = os.path.join(

+             self.path, "repos", "tickets", "test_ticket_repo.git"

+         )

+         output = pagure.lib.git.read_git_lines(

+             ["log", "-3", "--pretty='%H'"], gitrepo

+         )

+         self.assertEqual(len(output), 2)

+ 

+         hash0 = output[0].replace("'", "")

+         hash1 = output[1].replace("'", "")

+ 

+         # Case 0: EMPTY_TREE => hash0

+         output0 = pagure.lib.git.get_changed_files(

+             hash0, "^" + 40 * "0", gitrepo

+         )

+         self.assertEqual(output0, {issue.uid: "A"})

+ 

+         # Case 1: hash1 => hash0

+         output1 = pagure.lib.git.get_changed_files(hash0, hash1, gitrepo)

+         self.assertEqual(output1, {issue.uid: "M"})

+ 

+         # Case 2: hash0 => hash1

+         output2 = pagure.lib.git.get_changed_files(hash1, hash0, gitrepo)

+         self.assertEqual(output2, {issue.uid: "M"})

+ 

+         # Case 3: hash0 => hash0

+         output3 = pagure.lib.git.get_changed_files(hash0, hash0, gitrepo)

+         self.assertEqual(output3, {})

+ 

+         # Case 4: NULL => hash0

+         output4 = pagure.lib.git.get_changed_files(hash0, "", gitrepo)

+         self.assertEqual(output4, {})

+ 

+         # Case 5: NULL => NULL

+         output5 = pagure.lib.git.get_changed_files("", "", gitrepo)

+         self.assertEqual(output5, {})

+ 

      def test_get_repo_name(self):

          """ Test the get_repo_name method of pagure.lib.git. """

  

@@ -38,13 +38,15 @@ 

      def init_test_repo(self):

          tests.add_content_git_repo(self.projects[0])

          repo = pygit2.Repository(self.projects[0])

-         sha = repo.references["refs/heads/master"].peel().hex

+         commit = repo.references["refs/heads/master"].peel()

+         sha = commit.hex

+         oldsha = commit.parents[0].hex

          project = pagure.lib.query.get_authorized_project(self.session, "test")

-         return project, sha

+         return project, sha, oldsha

  

      @mock.patch("pagure.hooks.default.send_fedmsg_notifications")

      def test_send_action_notification(self, fedmsg):

-         project, sha = self.init_test_repo()

+         project, sha, _ = self.init_test_repo()

          pagure.hooks.default.send_action_notification(

              self.session,

              "tag",
@@ -62,8 +64,7 @@ 

  

      @mock.patch("pagure.hooks.default.send_fedmsg_notifications")

      def test_send_notifications(self, fedmsg):

-         oldrev = "9e5f51c951c6cab20fe81419320ed740533e2f2f"

-         project, sha = self.init_test_repo()

+         project, sha, oldsha = self.init_test_repo()

          pagure.hooks.default.send_notifications(

              self.session,

              project,
@@ -72,14 +73,23 @@ 

              "master",

              [sha],

              False,

-             oldrev,

+             oldsha,

+             None,

          )

          (_, args, kwargs) = fedmsg.mock_calls[0]

          self.assertEqual(args[1], "git.receive")

          self.assertEqual(args[2]["repo"]["name"], "test")

          self.assertEqual(args[2]["start_commit"], sha)

          self.assertEqual(args[2]["forced"], False)

-         self.assertEqual(args[2]["old_commit"], oldrev)

+         self.assertEqual(args[2]["old_commit"], oldsha)

+         self.assertEqual(

+             args[2]["changed_files"],

+             {

+                 "folder1/folder2/file": "A",

+                 "folder1/folder2/fileŠ": "A",

+             },

+         )

+         self.assertIsNone(args[2]["pull_request_id"])

  

  

  if __name__ == "__main__":

Pull request to prepare the new release 5.14.1.
Merge against 5.14.x branch.

Fixes: https://pagure.io/pagure/issue/5473

Metadata Update from @wombelix:
- Request assigned

a month ago

1 new commit added

  • Added data-toggle attribute to missing tooltips (Issue #4965)
a month ago

3 new commits added

  • Link to the file in the file history page breadcrumb
  • Fix the broken breadcrumb path in the file history page
  • Fix the breadcrumb styles for the file history page
a month ago

4 new commits added

  • Add history button to the tree view (fixes #5173)
  • Remove duplicate condition
  • file.html: Simplify template
  • file.html: Add comment with template name, simplify markup
a month ago

31 new commits added

  • Reorder celery arguments (and s/info/INFO/)
  • fix: dialect 'postgres://' deprecated in sqlalchemy
  • fix: BROKER_URL to honor REDIS_PORT and REDIS_DB
  • Add config option to restrict creating by OIDC groups
  • Add some basic documentation for boards
  • Fix grammar issue
  • Update Translation Status button
  • Drop the ssh key from the information stored in the cookie
  • Fix for mysql alembic migration
  • Document MySQL migration issue
  • Ensuring integer for mqtt port, needed by paho-mqtt
  • Change “Browse All” emoji link
  • Fix package unretirement URL
  • Add new monitoring options for release monitoring
  • Fix error when logging exception on event listener
  • Add API endpoint for reopening pull requests
  • refactor(mirror_project_in): address 'flake8' findings
  • refactor(mirror_project_in): reformating based on 'black' findings
  • fix(mirror_project_in): CLI desc adjusted to reflect script purpose
  • fix(mirror_project_in): unused 'check' argument removed
  • docs(install): filename apache sample config wrong
  • fix(theme): long words in source nav break layout
  • fix(user_settings): unable to change default email
  • Fix query filter for date ranges
  • cli/admin: Shorten message to fit length rule for the style check
  • fix: grammar of "log in"
  • Update chatroom reference to the new official Matrix room
  • Docs: Remove deprecated smart_strong xtn
  • Enable pull_request_update acl for user tokens
  • Enable collaborators to merge pull requests
  • English improvements
a month ago

2 new commits added

  • fix: Crash when config:[ENABLE_DOCS = False]
  • fix: Extra whitespace in "packages" on src.fpo front page
a month ago

27 new commits added

  • Use the correct status_code in test
  • Add test for cross-project API token
  • Allow PR author to close PR using API
  • Don't assume project token for PR close
  • Add api endpoints for adding/removing user to group
  • Fix unit-tests
  • Ensure the url we redirect to are full URLs
  • (#2127569) Fixes TypeError: BaseEventLoop.create_server() got an unexpected keyword argument 'loop'
  • Fix warning messages that lead to errors:
  • Allow author to update PR but not to change assignee
  • Owner of a PR can update is own PR
  • Update pagure CI guide
  • Add Global security step to Jenkins CI configuration
  • Add API key information to Jenkins CI setup
  • Fix object of "rebased onto" comment
  • Fix typo on pagure_authorized_keys_worker.service
  • fix: cannot import name 'escape' from 'jinja2'
  • Add pull request ID to push notification payload
  • Update pagure/themes/srcfpo/templates/repo_info.html
  • dist-git: Added a condition that decides whether to use the stg or prod version of the link
  • feat: added request for frozen branches and formating the function to have correct tab size
  • dist-git: pdc requested has beed replaced by the use of bodhi endpoint as a part of pdc decommission
  • Fix pagure.lib.git.get_changed_files()
  • Add changed files to push notification payload
  • Implement pagure.lib.git.get_changed_files()
  • Support Redis Unix sockets
  • Remove GIT_CURL_VERBOSE=1 from git commands
a month ago

10 new commits added

  • docs(changelog): Add links to related pull requests to security fixes in 5.14.1
  • chore: Bump version in 'pagure/__init__.py' and 'files/pagure.spec'
  • tests(15.4.1): Display short test summary after pytest run
  • fix: Mitigate bug in flask-wtf 0.14.2 on EL8 for backward compatbility of pagure release 5.14.1
  • docs(contributors): list updated for release 5.14.1
  • fix: generate_archive() follows symbolic links in temporary clones
  • fix: _update_file_in_git() follows symbolic links in temporary clones
  • fix: Path traversal in view_issue_raw_file()
  • Separate options and operands in PagureRepo.log()
  • docs(changelog): Release 5.14.1 added
a month ago

Pull-Request has been merged by wombelix

a month ago
Metadata
Changes Summary 119
+2 -2
file changed
README.rst
+8 -0
file changed
UPGRADING.rst
+2 -0
file changed
alembic/versions/c0bffa4e8fbc_token_if_in_commit_flag_can_be_null.py
+2 -2
file changed
dev/ansible/roles/pagure-dev/files/pagure.cfg
+1 -1
file changed
dev/ansible/roles/pagure-dev/files/pagure_authorized_keys_worker.service
+1 -1
file changed
dev/ansible/roles/pagure-dev/files/pagure_ci.service
+1 -1
file changed
dev/ansible/roles/pagure-dev/files/pagure_webhook.service
+1 -1
file changed
dev/ansible/roles/pagure-dev/files/pagure_worker.service
+1 -1
file changed
dev/containers/fedora-pip-py3
+1 -1
file changed
dev/containers/fedora-rpms-py3
+1 -1
file changed
dev/containers/logcom
+2 -2
file changed
dev/containers/runtests_py3.sh
+1 -1
file changed
dev/containers/tox_py3.sh
+1 -1
file changed
dev/containers/worker
+75 -0
file changed
doc/changelog.rst
+25 -6
file changed
doc/configuration.rst
+2 -2
file changed
doc/contributing.rst
+30 -11
file changed
doc/contributors.rst
+14 -14
file changed
doc/install.rst
+44
file added
doc/usage/board.rst
+4 -4
file changed
doc/usage/first_steps.rst
+1 -0
file changed
doc/usage/index.rst
+0 -1
file changed
doc/usage/markdown.rst
+29 -0
file changed
doc/usage/pagure_ci_jenkins.rst
+7 -0
file changed
doc/usage/project_settings.rst
+4 -14
file changed
files/mirror_project_in.py
+2 -2
file changed
files/pagure.cfg.sample
+4 -1
file changed
files/pagure.spec
+1 -1
file changed
files/pagure_authorized_keys_worker.service
+1 -1
file changed
files/pagure_ci.service
+1 -1
file changed
files/pagure_gitolite_worker.service
+1 -1
file changed
files/pagure_loadjson.service
+1 -1
file changed
files/pagure_logcom.service
+1 -1
file changed
files/pagure_mirror.service
+1 -1
file changed
files/pagure_webhook.service
+1 -1
file changed
files/pagure_worker.service
+1 -1
file changed
files/pagure_worker.service.example
+1 -3
file changed
pagure-ev/pagure_stream_server.py
+1 -1
file changed
pagure/__init__.py
+14 -1
file changed
pagure/api/__init__.py
+74 -3
file changed
pagure/api/fork.py
+179 -0
file changed
pagure/api/group.py
+5 -0
file changed
pagure/api/project.py
+7 -3
file changed
pagure/api/utils.py
+175 -0
file changed
pagure/cli/admin.py
+4 -0
file changed
pagure/config.py
+11 -0
file changed
pagure/default_config.py
+1 -1
file changed
pagure/doc/api.rst
+10 -5
file changed
pagure/flask_app.py
+24 -4
file changed
pagure/hooks/__init__.py
+13 -2
file changed
pagure/hooks/default.py
+3 -1
file changed
pagure/hooks/mail.py
+3 -1
file changed
pagure/hooks/mirror_hook.py
+3 -1
file changed
pagure/hooks/pagure_force_commit.py
+3 -1
file changed
pagure/hooks/pagure_hook.py
+3 -1
file changed
pagure/hooks/pagure_no_new_branches.py
+3 -1
file changed
pagure/hooks/pagure_request_hook.py
+3 -1
file changed
pagure/hooks/pagure_ticket_hook.py
+3 -1
file changed
pagure/hooks/pagure_unsigned_commits.py
+4 -2
file changed
pagure/hooks/rtd.py
+40 -4
file changed
pagure/lib/git.py
+1 -1
file changed
pagure/lib/notify.py
+14 -5
file changed
pagure/lib/query.py
+1 -0
file changed
pagure/lib/repo.py
+11 -2
file changed
pagure/lib/tasks.py
+11 -2
file changed
pagure/lib/tasks_mirror.py
+11 -2
file changed
pagure/lib/tasks_services.py
+1 -1
file changed
pagure/static/emoji/emojicomplete.js
+1 -1
file changed
pagure/static/issue_ev.js
+14 -14
file changed
pagure/templates/_access_levels_descriptions.html
+3 -3
file changed
pagure/templates/_formhelper.html
+1 -1
file changed
pagure/templates/commit.html
+2 -0
file changed
pagure/templates/commits.html
+1 -1
file changed
pagure/templates/edit_group.html
+25 -18
file changed
pagure/templates/file.html
+9 -14
file changed
pagure/templates/file_history.html
+1 -1
file changed
pagure/templates/group_info.html
+1 -1
file changed
pagure/templates/index.html
+5 -5
file changed
pagure/templates/issue.html
+3 -0
file changed
pagure/templates/master.html
+1 -1
file changed
pagure/templates/pull_request_comment.html
+1 -1
file changed
pagure/templates/repo_branches.html
+1 -0
file changed
pagure/templates/repo_comparecommits.html
+2 -1
file changed
pagure/templates/repo_new_pull_request.html
+4 -3
file changed
pagure/templates/repo_pull_request.html
+22 -22
file changed
pagure/templates/settings.html
+4 -4
file changed
pagure/templates/settings_api_keys.html
+1 -1
file changed
pagure/templates/unauthorized.html
+12 -11
file changed
pagure/templates/user_settings.html
+0 -0
file changed
pagure/themes/srcfpo/static/icons/transtats.png
+6 -1
file changed
pagure/themes/srcfpo/static/theme.css
+1 -1
file changed
pagure/themes/srcfpo/templates/group_info.html
+87 -34
file changed
pagure/themes/srcfpo/templates/repo_branches.html
+10 -10
file changed
pagure/themes/srcfpo/templates/repo_info.html
+69 -7
file changed
pagure/themes/srcfpo/templates/repo_master_sidebar.html
+6 -2
file changed
pagure/ui/app.py
+5 -0
file changed
pagure/ui/fas_login.py
+6 -3
file changed
pagure/ui/filters.py
+3 -0
file changed
pagure/ui/groups.py
+9 -3
file changed
pagure/ui/issues.py
+2 -0
file changed
pagure/ui/login.py
+11 -0
file changed
pagure/ui/oidc_login.py
+3 -2
file changed
pagure/ui/repo.py
+1 -1
file changed
run_ci_tests_containers.sh
+1 -1
file changed
runworker.py
+2 -0
file changed
tests/test_pagure_admin.py
+177 -0
file changed
tests/test_pagure_flask_api_fork.py
+234 -0
file changed
tests/test_pagure_flask_api_fork_update.py
+385 -0
file changed
tests/test_pagure_flask_api_group.py
+6 -6
file changed
tests/test_pagure_flask_api_user.py
+7 -1
file changed
tests/test_pagure_flask_rebase.py
+73 -1
file changed
tests/test_pagure_flask_ui_archives.py
+8 -8
file changed
tests/test_pagure_flask_ui_issues.py
+9 -9
file changed
tests/test_pagure_flask_ui_issues_acl_checks.py
+5 -5
file changed
tests/test_pagure_flask_ui_issues_open_access.py
+29 -7
file changed
tests/test_pagure_flask_ui_login.py
+31 -3
file changed
tests/test_pagure_lib.py
+47 -0
file changed
tests/test_pagure_lib_git.py
+17 -7
file changed
tests/test_pagure_send_notification.py