Imported Upstream version 2.3.4
340
LICENSE
Normal file
|
@ -0,0 +1,340 @@
|
|||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
License is intended to guarantee your freedom to share and change free
|
||||
software--to make sure the software is free for all its users. This
|
||||
General Public License applies to most of the Free Software
|
||||
Foundation's software and to any other program whose authors commit to
|
||||
using it. (Some other Free Software Foundation software is covered by
|
||||
the GNU Library General Public License instead.) You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
this service if you wish), that you receive source code or can get it
|
||||
if you want it, that you can change the software or use pieces of it
|
||||
in new free programs; and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
anyone to deny you these rights or to ask you to surrender the rights.
|
||||
These restrictions translate to certain responsibilities for you if you
|
||||
distribute copies of the software, or if you modify it.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must give the recipients all the rights that
|
||||
you have. You must make sure that they, too, receive or can get the
|
||||
source code. And you must show them these terms so they know their
|
||||
rights.
|
||||
|
||||
We protect your rights with two steps: (1) copyright the software, and
|
||||
(2) offer you this license which gives you legal permission to copy,
|
||||
distribute and/or modify the software.
|
||||
|
||||
Also, for each author's protection and ours, we want to make certain
|
||||
that everyone understands that there is no warranty for this free
|
||||
software. If the software is modified by someone else and passed on, we
|
||||
want its recipients to know that what they have is not the original, so
|
||||
that any problems introduced by others will not reflect on the original
|
||||
authors' reputations.
|
||||
|
||||
Finally, any free program is threatened constantly by software
|
||||
patents. We wish to avoid the danger that redistributors of a free
|
||||
program will individually obtain patent licenses, in effect making the
|
||||
program proprietary. To prevent this, we have made it clear that any
|
||||
patent must be licensed for everyone's free use or not licensed at all.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License applies to any program or other work which contains
|
||||
a notice placed by the copyright holder saying it may be distributed
|
||||
under the terms of this General Public License. The "Program", below,
|
||||
refers to any such program or work, and a "work based on the Program"
|
||||
means either the Program or any derivative work under copyright law:
|
||||
that is to say, a work containing the Program or a portion of it,
|
||||
either verbatim or with modifications and/or translated into another
|
||||
language. (Hereinafter, translation is included without limitation in
|
||||
the term "modification".) Each licensee is addressed as "you".
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running the Program is not restricted, and the output from the Program
|
||||
is covered only if its contents constitute a work based on the
|
||||
Program (independent of having been made by running the Program).
|
||||
Whether that is true depends on what the Program does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Program's
|
||||
source code as you receive it, in any medium, provided that you
|
||||
conspicuously and appropriately publish on each copy an appropriate
|
||||
copyright notice and disclaimer of warranty; keep intact all the
|
||||
notices that refer to this License and to the absence of any warranty;
|
||||
and give any other recipients of the Program a copy of this License
|
||||
along with the Program.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy, and
|
||||
you may at your option offer warranty protection in exchange for a fee.
|
||||
|
||||
2. You may modify your copy or copies of the Program or any portion
|
||||
of it, thus forming a work based on the Program, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) You must cause the modified files to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
b) You must cause any work that you distribute or publish, that in
|
||||
whole or in part contains or is derived from the Program or any
|
||||
part thereof, to be licensed as a whole at no charge to all third
|
||||
parties under the terms of this License.
|
||||
|
||||
c) If the modified program normally reads commands interactively
|
||||
when run, you must cause it, when started running for such
|
||||
interactive use in the most ordinary way, to print or display an
|
||||
announcement including an appropriate copyright notice and a
|
||||
notice that there is no warranty (or else, saying that you provide
|
||||
a warranty) and that users may redistribute the program under
|
||||
these conditions, and telling the user how to view a copy of this
|
||||
License. (Exception: if the Program itself is interactive but
|
||||
does not normally print such an announcement, your work based on
|
||||
the Program is not required to print an announcement.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Program,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Program, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Program.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Program
|
||||
with the Program (or with a work based on the Program) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may copy and distribute the Program (or a work based on it,
|
||||
under Section 2) in object code or executable form under the terms of
|
||||
Sections 1 and 2 above provided that you also do one of the following:
|
||||
|
||||
a) Accompany it with the complete corresponding machine-readable
|
||||
source code, which must be distributed under the terms of Sections
|
||||
1 and 2 above on a medium customarily used for software interchange; or,
|
||||
|
||||
b) Accompany it with a written offer, valid for at least three
|
||||
years, to give any third party, for a charge no more than your
|
||||
cost of physically performing source distribution, a complete
|
||||
machine-readable copy of the corresponding source code, to be
|
||||
distributed under the terms of Sections 1 and 2 above on a medium
|
||||
customarily used for software interchange; or,
|
||||
|
||||
c) Accompany it with the information you received as to the offer
|
||||
to distribute corresponding source code. (This alternative is
|
||||
allowed only for noncommercial distribution and only if you
|
||||
received the program in object code or executable form with such
|
||||
an offer, in accord with Subsection b above.)
|
||||
|
||||
The source code for a work means the preferred form of the work for
|
||||
making modifications to it. For an executable work, complete source
|
||||
code means all the source code for all modules it contains, plus any
|
||||
associated interface definition files, plus the scripts used to
|
||||
control compilation and installation of the executable. However, as a
|
||||
special exception, the source code distributed need not include
|
||||
anything that is normally distributed (in either source or binary
|
||||
form) with the major components (compiler, kernel, and so on) of the
|
||||
operating system on which the executable runs, unless that component
|
||||
itself accompanies the executable.
|
||||
|
||||
If distribution of executable or object code is made by offering
|
||||
access to copy from a designated place, then offering equivalent
|
||||
access to copy the source code from the same place counts as
|
||||
distribution of the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
4. You may not copy, modify, sublicense, or distribute the Program
|
||||
except as expressly provided under this License. Any attempt
|
||||
otherwise to copy, modify, sublicense or distribute the Program is
|
||||
void, and will automatically terminate your rights under this License.
|
||||
However, parties who have received copies, or rights, from you under
|
||||
this License will not have their licenses terminated so long as such
|
||||
parties remain in full compliance.
|
||||
|
||||
5. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Program or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Program (or any work based on the
|
||||
Program), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Program or works based on it.
|
||||
|
||||
6. Each time you redistribute the Program (or any work based on the
|
||||
Program), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute or modify the Program subject to
|
||||
these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties to
|
||||
this License.
|
||||
|
||||
7. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Program at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Program by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Program.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under
|
||||
any particular circumstance, the balance of the section is intended to
|
||||
apply and the section as a whole is intended to apply in other
|
||||
circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system, which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
8. If the distribution and/or use of the Program is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Program under this License
|
||||
may add an explicit geographical distribution limitation excluding
|
||||
those countries, so that distribution is permitted only in or among
|
||||
countries not thus excluded. In such case, this License incorporates
|
||||
the limitation as if written in the body of this License.
|
||||
|
||||
9. The Free Software Foundation may publish revised and/or new versions
|
||||
of the General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Program
|
||||
specifies a version number of this License which applies to it and "any
|
||||
later version", you have the option of following the terms and conditions
|
||||
either of that version or of any later version published by the Free
|
||||
Software Foundation. If the Program does not specify a version number of
|
||||
this License, you may choose any version ever published by the Free Software
|
||||
Foundation.
|
||||
|
||||
10. If you wish to incorporate parts of the Program into other free
|
||||
programs whose distribution conditions are different, write to the author
|
||||
to ask for permission. For software which is copyrighted by the Free
|
||||
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||
make exceptions for this. Our decision will be guided by the two goals
|
||||
of preserving the free status of all derivatives of our free software and
|
||||
of promoting the sharing and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
||||
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
||||
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
||||
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
||||
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
||||
REPAIR OR CORRECTION.
|
||||
|
||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
||||
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
||||
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
convey the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program is interactive, make it output a short notice like this
|
||||
when it starts in an interactive mode:
|
||||
|
||||
Gnomovision version 69, Copyright (C) year name of author
|
||||
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, the commands you use may
|
||||
be called something other than `show w' and `show c'; they could even be
|
||||
mouse-clicks or menu items--whatever suits your program.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||
|
||||
<signature of Ty Coon>, 1 April 1989
|
||||
Ty Coon, President of Vice
|
||||
|
||||
This General Public License does not permit incorporating your program into
|
||||
proprietary programs. If your program is a subroutine library, you may
|
||||
consider it more useful to permit linking proprietary applications with the
|
||||
library. If this is what you want to do, use the GNU Library General
|
||||
Public License instead of this License.
|
10
MANIFEST.in
Normal file
|
@ -0,0 +1,10 @@
|
|||
include LICENSE README.rst requirements.txt UPGRADING.rst
|
||||
include createdb.py
|
||||
recursive-include pagure *
|
||||
recursive-include files *
|
||||
recursive-include milters *
|
||||
recursive-include tests *
|
||||
recursive-include doc *
|
||||
recursive-include alembic *
|
||||
recursive-include ev-server *
|
||||
recursive-include webhook-server *
|
11
PKG-INFO
Normal file
|
@ -0,0 +1,11 @@
|
|||
Metadata-Version: 1.1
|
||||
Name: pagure
|
||||
Version: 2.3.4
|
||||
Summary: A light-weight git-centered forge based on pygit2..
|
||||
Home-page: https://fedorahosted.org/pagure/
|
||||
Author: Pierre-Yves Chibon
|
||||
Author-email: pingou@pingoured.fr
|
||||
License: GPLv2+
|
||||
Download-URL: https://fedorahosted.org/releases/p/r/pagure/
|
||||
Description: UNKNOWN
|
||||
Platform: UNKNOWN
|
81
README.rst
Normal file
|
@ -0,0 +1,81 @@
|
|||
Pagure
|
||||
======
|
||||
|
||||
:Author: Pierre-Yves Chibon <pingou@pingoured.fr>
|
||||
|
||||
|
||||
Pagure is a git-centered forge, python based using pygit2.
|
||||
|
||||
With pagure you can host your project with its documentation, let your users
|
||||
report issues or request enhancements using the ticketing system and build your
|
||||
community of contributors by allowing them to fork your projects and contribute
|
||||
to it via the now-popular pull-request mechanism.
|
||||
|
||||
|
||||
Homepage: https://pagure.io/pagure
|
||||
|
||||
See it at work: https://pagure.io
|
||||
|
||||
|
||||
Playground version: https://stg.pagure.io
|
||||
|
||||
|
||||
|
||||
Get it running
|
||||
==============
|
||||
|
||||
* Install the needed system libraries::
|
||||
|
||||
sudo dnf install git python-virtualenv libgit2-devel \
|
||||
libjpeg-devel gcc libffi-devel redhat-rpm-config
|
||||
|
||||
.. note:: Do note the version of libgit2 that you install, for example
|
||||
in ``libgit2-0.23.4-1`` you need to keep in mind the ``0.23``
|
||||
|
||||
* Retrieve the sources::
|
||||
|
||||
git clone https://pagure.io/pagure.git
|
||||
cd pagure
|
||||
|
||||
* Install dependencies
|
||||
|
||||
* create the virtualenv::
|
||||
|
||||
virtualenv pagure_env
|
||||
source ./pagure_env/bin/activate
|
||||
|
||||
* Install the correct version of pygit2::
|
||||
|
||||
pip install pygit2==<version of libgit2 found>.*
|
||||
|
||||
So in our example::
|
||||
|
||||
pip install pygit2==0.23.*
|
||||
|
||||
* Install the rest of the dependencies::
|
||||
|
||||
pip install -r requirements.txt
|
||||
|
||||
|
||||
* Create the folder that will receive the projects, forks, docs, requests and
|
||||
tickets' git repo::
|
||||
|
||||
mkdir {repos,docs,forks,tickets,requests}
|
||||
|
||||
|
||||
* Create the inital database scheme::
|
||||
|
||||
python createdb.py
|
||||
|
||||
|
||||
* Run it::
|
||||
|
||||
./runserver.py
|
||||
|
||||
|
||||
* To get some profiling information you can also run it as::
|
||||
|
||||
./runserver.py --profile
|
||||
|
||||
|
||||
This will launch the application at http://127.0.0.1:5000
|
187
UPGRADING.rst
Normal file
|
@ -0,0 +1,187 @@
|
|||
Upgrading Pagure
|
||||
================
|
||||
|
||||
|
||||
2.3.4
|
||||
-----
|
||||
|
||||
Release 2.3.4 contains an important security fix, blocking a source of XSS
|
||||
attack. (CVE-2016-1000037)
|
||||
|
||||
|
||||
|
||||
From 2.2 to 2.3
|
||||
---------------
|
||||
|
||||
2.3 brings a few changes impacting the database scheme, including a new
|
||||
`duplicate` status for tickets, a feature allowing one to `watch` or
|
||||
`unwatch` a project and notifications on tickets as exist on pull-requests.
|
||||
|
||||
Therefore, when upgrading from 2.2.x to 2.3, you will have to :
|
||||
|
||||
* Create the new DB tables and the new status field using the ``createdb.py`` script.
|
||||
|
||||
* Update the database schame using alembic: ``alembic upgrade head``
|
||||
|
||||
This update also brings a new configuration key:
|
||||
|
||||
* ``PAGURE_ADMIN_USERS`` allows to mark some users as instance-wide admins, giving
|
||||
them full access to every projects, private or not. This feature can then be
|
||||
used as a way to clean spams.
|
||||
* ``SMTP_PORT`` allows to specify the port to use when contacting the SMTP
|
||||
server
|
||||
* ``SMTP_SSL`` allows to specify whether to use SSL when contacting the SMTP
|
||||
server
|
||||
* ``SMTP_USERNAME`` and ``SMTP_PASSWORD`` if provided together allow to contact
|
||||
an SMTP requiring authentication.
|
||||
|
||||
In this update is also added the script ``api_key_expire_mail.py`` meant to be
|
||||
run by a daily cron job and warning users when their API token is nearing its
|
||||
expiration date.
|
||||
|
||||
|
||||
|
||||
2.2.2
|
||||
-----
|
||||
|
||||
Release 2.2.2 contains an important security fix, blocking a source of XSS
|
||||
attack.
|
||||
|
||||
|
||||
|
||||
From 2.1 to 2.2
|
||||
---------------
|
||||
|
||||
2.2 brings a number of bug fixes and a few improvements.
|
||||
|
||||
One of the major changes impacts the databases where we must change some of the
|
||||
table so that the foreign key cascade on delete (fixes deleting a project when a
|
||||
few plugins were activated).
|
||||
|
||||
When upgrading for 2.1 to 2.2 all you will have to do is:
|
||||
|
||||
* Update the database scheme using alembic: ``alembic upgrade head``
|
||||
|
||||
.. note:: If you run another database system than PostgreSQL the alembic
|
||||
revision ``317a285e04a8_delete_hooks.py`` will require adjustment as the
|
||||
foreign key constraints are named and the names are driver dependant.
|
||||
|
||||
|
||||
|
||||
From 2.0 to 2.1
|
||||
---------------
|
||||
|
||||
2.1 brings its usual flow of improvements and bug fixes.
|
||||
|
||||
When upgrading from 2.0.x to 2.1 all you will have to:
|
||||
|
||||
* Update the database schame using alembic: ``alembic upgrade head``
|
||||
|
||||
|
||||
|
||||
From 1.x to 2.0
|
||||
---------------
|
||||
|
||||
As the version change indicates, 2.0 brings quite a number of changes,
|
||||
including some that are not backward compatible.
|
||||
|
||||
When upgrading to 2.0 you will have to:
|
||||
|
||||
* Update the database schema using alembic: ``alembic upgrade head``
|
||||
|
||||
* Create the new DB tables so that the new plugins work using the
|
||||
``createdb.py`` script
|
||||
|
||||
* Move the forks git repo
|
||||
|
||||
Forked git repos are now located under the same folder as the regular git
|
||||
repos, just under a ``forks/`` subfolder.
|
||||
So the structure changes from: ::
|
||||
|
||||
repos/
|
||||
├── foo.git
|
||||
└── bar.git
|
||||
|
||||
forks/
|
||||
├── patrick/
|
||||
│ ├── test.git
|
||||
│ └── ipsilon.git
|
||||
└── pingou/
|
||||
├── foo.git
|
||||
└── bar.git
|
||||
|
||||
to: ::
|
||||
|
||||
repos/
|
||||
├── foo.git
|
||||
├── bar.git
|
||||
└── forks/
|
||||
├── patrick/
|
||||
│ ├── test.git
|
||||
│ └── ipsilon.git
|
||||
└── pingou/
|
||||
├── foo.git
|
||||
└── bar.git
|
||||
|
||||
So the entire ``forks`` folder is moved under the ``repos`` folder where
|
||||
the other repositories are, containing the sources of the projects.
|
||||
|
||||
|
||||
Git repos for ``tickets``, ``requests`` and ``docs`` will be trickier to
|
||||
move as the structure changes from: ::
|
||||
|
||||
tickets/
|
||||
├── foo.git
|
||||
├── bar.git
|
||||
├── patrick/
|
||||
│ ├── test.git
|
||||
│ └── ipsilon.git
|
||||
└── pingou/
|
||||
├── foo.git
|
||||
└── bar.git
|
||||
|
||||
to: ::
|
||||
|
||||
tickets/
|
||||
├── foo.git
|
||||
├── bar.git
|
||||
└── forks/
|
||||
├── patrick/
|
||||
│ ├── test.git
|
||||
│ └── ipsilon.git
|
||||
└── pingou/
|
||||
├── foo.git
|
||||
└── bar.git
|
||||
|
||||
Same for the ``requests`` and the ``docs`` git repos.
|
||||
|
||||
As you can see in the ``tickets``, ``requests`` and ``docs`` folders there
|
||||
are two types of folders, git repos which are folder with a name ending
|
||||
with ``.git``, and folder corresponding to usernames. These last ones are
|
||||
the ones to be moved into a subfolder ``forks/``.
|
||||
|
||||
This can be done using something like: ::
|
||||
|
||||
mkdir forks
|
||||
for i in `ls -1 |grep -v '\.git'`; do mv $i forks/; done
|
||||
|
||||
* Re-generate the gitolite configuration.
|
||||
|
||||
This can be done via the ``Re-generate gitolite ACLs file`` button in the
|
||||
admin page.
|
||||
|
||||
* Keep URLs backward compatible
|
||||
|
||||
The support of pseudo-namespace in pagure 2.0 has required some changes
|
||||
to the URL schema:
|
||||
https://pagure.io/pagure/053d8cc95fcd50c23a8b0a7f70e55f8d1cc7aebb
|
||||
became:
|
||||
https://pagure.io/pagure/c/053d8cc95fcd50c23a8b0a7f70e55f8d1cc7aebb
|
||||
(Note the added /c/ in it)
|
||||
|
||||
We introduced a backward compatibility fix for this.
|
||||
|
||||
This fix is however *disabled* by default so if you wish to keep the URLs
|
||||
valid, you will need to adjust you configuration file to include: ::
|
||||
|
||||
OLD_VIEW_COMMIT_ENABLED = True
|
71
alembic/env.py
Normal file
|
@ -0,0 +1,71 @@
|
|||
from __future__ import with_statement
|
||||
from alembic import context
|
||||
from sqlalchemy import engine_from_config, pool
|
||||
from logging.config import fileConfig
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
config = context.config
|
||||
|
||||
# Interpret the config file for Python logging.
|
||||
# This line sets up loggers basically.
|
||||
fileConfig(config.config_file_name)
|
||||
|
||||
# add your model's MetaData object here
|
||||
# for 'autogenerate' support
|
||||
# from myapp import mymodel
|
||||
# target_metadata = mymodel.Base.metadata
|
||||
target_metadata = None
|
||||
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
# can be acquired:
|
||||
# my_important_option = config.get_main_option("my_important_option")
|
||||
# ... etc.
|
||||
|
||||
def run_migrations_offline():
|
||||
"""Run migrations in 'offline' mode.
|
||||
|
||||
This configures the context with just a URL
|
||||
and not an Engine, though an Engine is acceptable
|
||||
here as well. By skipping the Engine creation
|
||||
we don't even need a DBAPI to be available.
|
||||
|
||||
Calls to context.execute() here emit the given string to the
|
||||
script output.
|
||||
|
||||
"""
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(url=url, target_metadata=target_metadata)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
def run_migrations_online():
|
||||
"""Run migrations in 'online' mode.
|
||||
|
||||
In this scenario we need to create an Engine
|
||||
and associate a connection with the context.
|
||||
|
||||
"""
|
||||
engine = engine_from_config(
|
||||
config.get_section(config.config_ini_section),
|
||||
prefix='sqlalchemy.',
|
||||
poolclass=pool.NullPool)
|
||||
|
||||
connection = engine.connect()
|
||||
context.configure(
|
||||
connection=connection,
|
||||
target_metadata=target_metadata
|
||||
)
|
||||
|
||||
try:
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
||||
|
22
alembic/script.py.mako
Normal file
|
@ -0,0 +1,22 @@
|
|||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = ${repr(up_revision)}
|
||||
down_revision = ${repr(down_revision)}
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
def upgrade():
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade():
|
||||
${downgrades if downgrades else "pass"}
|
45
alembic/versions/15ea3c2cf83d_pr_comment_editing.py
Normal file
|
@ -0,0 +1,45 @@
|
|||
"""Adding column to store edited_by and edited_on a PR comment
|
||||
|
||||
Revision ID: 15ea3c2cf83d
|
||||
Revises: 1cd0a853c697
|
||||
Create Date: 2015-11-09 16:18:47.192088
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '15ea3c2cf83d'
|
||||
down_revision = '1cd0a853c697'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
''' Add the columns editor_id and edited_on to the table
|
||||
pull_request_comments.
|
||||
'''
|
||||
|
||||
op.add_column(
|
||||
'pull_request_comments',
|
||||
sa.Column(
|
||||
'editor_id',
|
||||
sa.Integer,
|
||||
sa.ForeignKey('users.id', onupdate='CASCADE'),
|
||||
nullable=True)
|
||||
)
|
||||
|
||||
op.add_column(
|
||||
'pull_request_comments',
|
||||
sa.Column(
|
||||
'edited_on',
|
||||
sa.DateTime,
|
||||
nullable=True)
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
''' Remove the columns editor_id and edited_on from the table
|
||||
pull_request_comments.
|
||||
'''
|
||||
op.drop_column('pull_request_comments', 'editor_id')
|
||||
op.drop_column('pull_request_comments', 'edited_on')
|
38
alembic/versions/1b6d7dc5600a_versioning_passwords.py
Normal file
|
@ -0,0 +1,38 @@
|
|||
"""versioning_passwords
|
||||
|
||||
Revision ID: 1b6d7dc5600a
|
||||
Revises: 3b441ef4e928
|
||||
Create Date: 2016-01-13 07:57:23.465676
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '1b6d7dc5600a'
|
||||
down_revision = '3b441ef4e928'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import sqlalchemy.orm
|
||||
|
||||
try:
|
||||
from pagure.lib import model
|
||||
except ImportError:
|
||||
import sys
|
||||
sys.path.insert(0, '.')
|
||||
from pagure.lib import model
|
||||
|
||||
|
||||
def upgrade():
|
||||
engine = op.get_bind()
|
||||
Session = sqlalchemy.orm.scoped_session(sqlalchemy.orm.sessionmaker())
|
||||
Session.configure(bind=engine)
|
||||
session = Session()
|
||||
for user in session.query(model.User).filter(
|
||||
model.User.password != None).all():
|
||||
user.password = '$1$%s' % user.password
|
||||
session.add(user)
|
||||
session.commit()
|
||||
|
||||
|
||||
def downgrade():
|
||||
raise ValueError("Password can not be downgraded")
|
37
alembic/versions/1cd0a853c697_add_closed_at_field_in_pr.py
Normal file
|
@ -0,0 +1,37 @@
|
|||
"""Add closed_at field in PR
|
||||
|
||||
|
||||
Revision ID: 1cd0a853c697
|
||||
Revises: 6190226bed0
|
||||
Create Date: 2015-10-02 09:32:15.370676
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '1cd0a853c697'
|
||||
down_revision = '6190226bed0'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
''' Add the column closed_at to the table pull_requests.
|
||||
'''
|
||||
op.add_column(
|
||||
'pull_requests',
|
||||
sa.Column(
|
||||
'closed_at',
|
||||
sa.DateTime,
|
||||
nullable=True,
|
||||
)
|
||||
)
|
||||
|
||||
op.execute('''UPDATE "pull_requests" SET closed_at=date_created '''
|
||||
'''WHERE STATUS != 'Open';''')
|
||||
|
||||
|
||||
def downgrade():
|
||||
''' Remove the column closed_at from the table pull_requests.
|
||||
'''
|
||||
op.drop_column('pull_requests', 'closed_at')
|
|
@ -0,0 +1,29 @@
|
|||
"""Add the tree_id column to PR inline comments
|
||||
|
||||
Revision ID: 1f3de3853a1a
|
||||
Revises: 58e60d869326
|
||||
Create Date: 2016-02-22 16:13:59.943083
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '1f3de3853a1a'
|
||||
down_revision = '58e60d869326'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
''' Add the column tree_id to the table pull_request_comments.
|
||||
'''
|
||||
op.add_column(
|
||||
'pull_request_comments',
|
||||
sa.Column('tree_id', sa.String(40), nullable=True)
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
''' Remove the column tree_id from the table pull_request_comments.
|
||||
'''
|
||||
op.drop_column('pull_request_comments', 'tree_id')
|
|
@ -0,0 +1,33 @@
|
|||
"""Add notifications to tickets
|
||||
|
||||
Revision ID: 22db0a833d35
|
||||
Revises: 317a285e04a8
|
||||
Create Date: 2016-06-27 16:10:33.395495
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '22db0a833d35'
|
||||
down_revision = '317a285e04a8'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
''' Add the column notification to the table issue_comments.
|
||||
'''
|
||||
op.add_column(
|
||||
'issue_comments',
|
||||
sa.Column('notification', sa.Boolean, default=False, nullable=True)
|
||||
)
|
||||
op.execute('''UPDATE "issue_comments" SET notification=False;''')
|
||||
op.alter_column(
|
||||
'issue_comments', 'notification',
|
||||
nullable=False, existing_nullable=True)
|
||||
|
||||
|
||||
def downgrade():
|
||||
''' Remove the column notification from the table issue_comments.
|
||||
'''
|
||||
op.drop_column('issue_comments', 'notification')
|
46
alembic/versions/257a7ce22682_add_the_remote_git_entry.py
Normal file
|
@ -0,0 +1,46 @@
|
|||
"""Add the remote_git entry
|
||||
|
||||
Revision ID: 257a7ce22682
|
||||
Revises: 36116bb7a69b
|
||||
Create Date: 2015-07-21 14:26:23.989220
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '257a7ce22682'
|
||||
down_revision = '36116bb7a69b'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
''' Add the column remote_git to the table pull_requests and make the
|
||||
project_id_from field nullable.
|
||||
'''
|
||||
op.add_column(
|
||||
'pull_requests',
|
||||
sa.Column('remote_git', sa.Text, nullable=True)
|
||||
)
|
||||
op.alter_column(
|
||||
'pull_requests',
|
||||
column_name='project_id_from',
|
||||
nullable=True,
|
||||
existing_nullable=False)
|
||||
op.create_check_constraint(
|
||||
"ck_lcl_or_remo_pr",
|
||||
"pull_requests",
|
||||
'NOT(project_id_from IS NULL AND remote_git IS NULL)'
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
''' Remove the column remote_git from the table pull_requests and make
|
||||
the project_id_from field not nullable.
|
||||
'''
|
||||
op.drop_column('pull_requests', 'remote_git')
|
||||
op.alter_column(
|
||||
'pull_requests',
|
||||
column_name='project_id_from',
|
||||
nullable=False,
|
||||
existing_nullable=True)
|
|
@ -0,0 +1,60 @@
|
|||
"""Change the status of pull_requests
|
||||
|
||||
|
||||
Revision ID: 298891e63039
|
||||
Revises: 3c25e14b855b
|
||||
Create Date: 2015-06-08 13:06:11.938966
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '298891e63039'
|
||||
down_revision = '3c25e14b855b'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
''' Adjust the status column of the pull_requests table.
|
||||
'''
|
||||
op.add_column(
|
||||
'pull_requests',
|
||||
sa.Column(
|
||||
'_status', sa.Text,
|
||||
sa.ForeignKey(
|
||||
'status_pull_requests.status', onupdate='CASCADE'),
|
||||
default='Open',
|
||||
nullable=True)
|
||||
)
|
||||
|
||||
op.execute('''UPDATE "pull_requests" '''
|
||||
'''SET _status='Open' WHERE status=TRUE;''')
|
||||
op.execute('''UPDATE "pull_requests" '''
|
||||
'''SET _status='Merged' WHERE status=FALSE;''')
|
||||
|
||||
op.drop_column('pull_requests', 'status')
|
||||
op.alter_column(
|
||||
'pull_requests',
|
||||
column_name='_status', new_column_name='status',
|
||||
nullable=False, existing_nullable=True)
|
||||
|
||||
|
||||
def downgrade():
|
||||
''' Revert the status column of the pull_requests table.
|
||||
'''
|
||||
op.add_column(
|
||||
'pull_requests',
|
||||
sa.Column(
|
||||
'_status', sa.Boolean, default=True, nullable=True)
|
||||
)
|
||||
op.execute('''UPDATE "pull_requests" '''
|
||||
'''SET _status=TRUE WHERE status='Open';''')
|
||||
op.execute('''UPDATE "pull_requests" '''
|
||||
'''SET _status=FALSE WHERE status!='Open';''')
|
||||
|
||||
op.drop_column('pull_requests', 'status')
|
||||
op.alter_column(
|
||||
'pull_requests',
|
||||
column_name='_status', new_column_name='status',
|
||||
nullable=False, existing_nullable=True)
|
31
alembic/versions/2aa7b3958bc5_add_the_milestones_column.py
Normal file
|
@ -0,0 +1,31 @@
|
|||
"""Add the milestones column
|
||||
|
||||
Revision ID: 2aa7b3958bc5
|
||||
Revises: 443e090da188
|
||||
Create Date: 2016-05-03 15:59:04.992414
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '2aa7b3958bc5'
|
||||
down_revision = '443e090da188'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
''' Add the column _milestones to the table projects
|
||||
and the column milestone to the table issues.
|
||||
'''
|
||||
op.add_column(
|
||||
'projects',
|
||||
sa.Column('_milestones', sa.Text, nullable=True)
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
''' Drop the column _milestones from the table projects
|
||||
and the column milestone from the table issues.
|
||||
'''
|
||||
op.drop_column('projects', '_milestones')
|
59
alembic/versions/317a285e04a8_delete_hooks.py
Normal file
|
@ -0,0 +1,59 @@
|
|||
"""Delete hooks
|
||||
|
||||
Revision ID: 317a285e04a8
|
||||
Revises: 2aa7b3958bc5
|
||||
Create Date: 2016-05-30 11:28:48.512577
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '317a285e04a8'
|
||||
down_revision = '2aa7b3958bc5'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
""" Alter the hooks table to update the foreign key to cascade on delete.
|
||||
"""
|
||||
|
||||
for table in [
|
||||
'hook_fedmsg', 'hook_irc', 'hook_mail',
|
||||
'hook_pagure_force_commit', 'hook_pagure', 'hook_pagure_requests',
|
||||
'hook_pagure_tickets', 'hook_pagure_unsigned_commit', 'hook_rtd',
|
||||
]:
|
||||
op.drop_constraint(
|
||||
'%s_project_id_fkey' % table,
|
||||
table,
|
||||
type_='foreignkey')
|
||||
op. create_foreign_key(
|
||||
name='%s_project_id_fkey' % table,
|
||||
source_table=table,
|
||||
referent_table='projects',
|
||||
local_cols=['project_id'],
|
||||
remote_cols=['id'],
|
||||
onupdate='cascade',
|
||||
ondelete='cascade',
|
||||
)
|
||||
|
||||
op.drop_constraint(
|
||||
'projects_groups_project_id_fkey',
|
||||
'projects_groups',
|
||||
type_='foreignkey')
|
||||
op. create_foreign_key(
|
||||
name='projects_groups_project_id_fkey',
|
||||
source_table='projects_groups',
|
||||
referent_table='projects',
|
||||
local_cols=['project_id'],
|
||||
remote_cols=['id'],
|
||||
onupdate='cascade',
|
||||
ondelete='cascade',
|
||||
)
|
||||
|
||||
|
||||
|
||||
def downgrade():
|
||||
""" Alter the hooks table to update the foreign key to undo the cascade
|
||||
on delete.
|
||||
"""
|
|
@ -0,0 +1,29 @@
|
|||
"""Add the url field to project
|
||||
|
||||
Revision ID: 36116bb7a69b
|
||||
Revises: abc71fd60fa
|
||||
Create Date: 2015-06-11 12:36:33.544046
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '36116bb7a69b'
|
||||
down_revision = 'abc71fd60fa'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
''' Add the column url to the table projects.
|
||||
'''
|
||||
op.add_column(
|
||||
'projects',
|
||||
sa.Column('url', sa.Text, nullable=True)
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
''' Remove the column merge_status from the table projects.
|
||||
'''
|
||||
op.drop_column('projects', 'url')
|
44
alembic/versions/3b441ef4e928_comment_editing_issue.py
Normal file
|
@ -0,0 +1,44 @@
|
|||
"""Adding column to store edited_by and edited_on a issue comment
|
||||
|
||||
Revision ID: 3b441ef4e928
|
||||
Revises: 15ea3c2cf83d
|
||||
Create Date: 2015-12-03 12:34:28.316699
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '3b441ef4e928'
|
||||
down_revision = '15ea3c2cf83d'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
''' Add the columns editor_id and edited_on to the table issue_comments.
|
||||
'''
|
||||
|
||||
op.add_column(
|
||||
'issue_comments',
|
||||
sa.Column(
|
||||
'editor_id',
|
||||
sa.Integer,
|
||||
sa.ForeignKey('users.id', onupdate='CASCADE'),
|
||||
nullable=True)
|
||||
)
|
||||
|
||||
op.add_column(
|
||||
'issue_comments',
|
||||
sa.Column(
|
||||
'edited_on',
|
||||
sa.DateTime,
|
||||
nullable=True)
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
''' Remove the columns editor_id and edited_on from the table
|
||||
issue_comments.
|
||||
'''
|
||||
op.drop_column('issue_comments', 'editor_id')
|
||||
op.drop_column('issue_comments', 'edited_on')
|
|
@ -0,0 +1,29 @@
|
|||
"""add an avatar email for project
|
||||
|
||||
Revision ID: 3c25e14b855b
|
||||
Revises: b5efae6bb23
|
||||
Create Date: 2015-06-08 12:05:13.832348
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '3c25e14b855b'
|
||||
down_revision = 'b5efae6bb23'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
''' Add the column merge_status to the table projects.
|
||||
'''
|
||||
op.add_column(
|
||||
'projects',
|
||||
sa.Column('avatar_email', sa.Text, nullable=True)
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
''' Remove the column merge_status from the table projects.
|
||||
'''
|
||||
op.drop_column('projects', 'avatar_email')
|
|
@ -0,0 +1,27 @@
|
|||
"""add_closed_at_attribute_in_issues
|
||||
|
||||
Revision ID: 43df5e588a87
|
||||
Revises: 22db0a833d35
|
||||
Create Date: 2016-06-28 22:59:36.653905
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '43df5e588a87'
|
||||
down_revision = '22db0a833d35'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
''' Add closed_at column in issues table '''
|
||||
op.add_column(
|
||||
'issues',
|
||||
sa.Column('closed_at', sa.DateTime, nullable=True)
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
''' Remove the closed_at column in issues table '''
|
||||
op.drop_column('issues', 'closed_at')
|
|
@ -0,0 +1,32 @@
|
|||
"""up to 255 characters for project.name
|
||||
|
||||
Revision ID: 443e090da188
|
||||
Revises: 496f7a700f2e
|
||||
Create Date: 2016-04-20 17:57:36.385103
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '443e090da188'
|
||||
down_revision = '496f7a700f2e'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.alter_column(
|
||||
table_name='projects',
|
||||
column_name='name',
|
||||
type_=sa.String(255),
|
||||
existing_type=sa.String(32)
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.alter_column(
|
||||
table_name='projects',
|
||||
column_name='name',
|
||||
type_=sa.String(32),
|
||||
existing_type=sa.String(255)
|
||||
)
|
37
alembic/versions/496f7a700f2e_add_priorities.py
Normal file
|
@ -0,0 +1,37 @@
|
|||
"""Add priorities
|
||||
|
||||
Revision ID: 496f7a700f2e
|
||||
Revises: 4cae55a80a42
|
||||
Create Date: 2016-03-24 12:19:34.298752
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '496f7a700f2e'
|
||||
down_revision = '4cae55a80a42'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
''' Add the column _priorities to the table projects
|
||||
and the column priority to the table issues.
|
||||
'''
|
||||
op.add_column(
|
||||
'projects',
|
||||
sa.Column('_priorities', sa.Text, nullable=True)
|
||||
)
|
||||
|
||||
op.add_column(
|
||||
'issues',
|
||||
sa.Column('priority', sa.Integer, nullable=True, default=None)
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
''' Drop the column _priorities from the table projects
|
||||
and the column priority from the table issues.
|
||||
'''
|
||||
op.drop_column('projects', '_priorities')
|
||||
op.drop_column('issues', 'priority')
|
|
@ -0,0 +1,29 @@
|
|||
"""Add the initial_comment on the PR table
|
||||
|
||||
Revision ID: 4cae55a80a42
|
||||
Revises: 1f3de3853a1a
|
||||
Create Date: 2016-03-01 12:00:34.823097
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '4cae55a80a42'
|
||||
down_revision = '1f3de3853a1a'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
''' Add the column initial_comment to the table pull_requests.
|
||||
'''
|
||||
op.add_column(
|
||||
'pull_requests',
|
||||
sa.Column('initial_comment', sa.Text, nullable=True)
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
''' Remove the column initial_comment from the table pull_requests.
|
||||
'''
|
||||
op.drop_column('pull_requests', 'initial_comment')
|
33
alembic/versions/58e60d869326_add_notification_bool_to_pr.py
Normal file
|
@ -0,0 +1,33 @@
|
|||
"""add notification bool to PR
|
||||
|
||||
Revision ID: 58e60d869326
|
||||
Revises: 1b6d7dc5600a
|
||||
Create Date: 2016-02-12 12:39:07.839530
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '58e60d869326'
|
||||
down_revision = '1b6d7dc5600a'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
''' Add the column notification to the table pull_request_comments.
|
||||
'''
|
||||
op.add_column(
|
||||
'pull_request_comments',
|
||||
sa.Column('notification', sa.Boolean, default=False, nullable=True)
|
||||
)
|
||||
op.execute('''UPDATE "pull_request_comments" SET notification=False;''')
|
||||
op.alter_column(
|
||||
'pull_request_comments', 'notification',
|
||||
nullable=False, existing_nullable=True)
|
||||
|
||||
|
||||
def downgrade():
|
||||
''' Remove the column notification from the table pull_request_comments.
|
||||
'''
|
||||
op.drop_column('pull_request_comments', 'notification')
|
|
@ -0,0 +1,43 @@
|
|||
"""Add the updated_on column to pull-requests
|
||||
|
||||
Revision ID: 6190226bed0
|
||||
Revises: 257a7ce22682
|
||||
Create Date: 2015-09-29 15:32:58.229183
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '6190226bed0'
|
||||
down_revision = '257a7ce22682'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
''' Add the column updated_on to the table pull_requests.
|
||||
'''
|
||||
op.add_column(
|
||||
'pull_requests',
|
||||
sa.Column(
|
||||
'updated_on',
|
||||
sa.DateTime,
|
||||
nullable=True,
|
||||
default=sa.func.now(),
|
||||
onupdate=sa.func.now()
|
||||
)
|
||||
)
|
||||
|
||||
op.execute('''UPDATE "pull_requests" SET updated_on=date_created;''')
|
||||
|
||||
op.alter_column(
|
||||
'pull_requests',
|
||||
column_name='updated_on',
|
||||
nullable=False,
|
||||
existing_nullable=True)
|
||||
|
||||
|
||||
def downgrade():
|
||||
''' Remove the column updated_on from the table pull_requests.
|
||||
'''
|
||||
op.drop_column('pull_requests', 'updated_on')
|
|
@ -0,0 +1,34 @@
|
|||
"""Add the closed_by column to pull_requests
|
||||
|
||||
|
||||
Revision ID: abc71fd60fa
|
||||
Revises: 298891e63039
|
||||
Create Date: 2015-06-08 16:06:18.017110
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'abc71fd60fa'
|
||||
down_revision = '298891e63039'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
''' Add the column merge_status to the table pull_requests.
|
||||
'''
|
||||
op.add_column(
|
||||
'pull_requests',
|
||||
sa.Column(
|
||||
'closed_by_id',
|
||||
sa.Integer,
|
||||
sa.ForeignKey('users.id', onupdate='CASCADE'),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
''' Remove the column merge_status from the table pull_requests.
|
||||
'''
|
||||
op.drop_column('pull_requests', 'closed_by_id')
|
|
@ -0,0 +1,35 @@
|
|||
"""Add merge status to the pull_requests table
|
||||
|
||||
Revision ID: b5efae6bb23
|
||||
Revises: None
|
||||
Create Date: 2015-06-02 16:30:06.199128
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'b5efae6bb23'
|
||||
down_revision = None
|
||||
|
||||
from alembic import op
|
||||
from sqlalchemy.dialects.postgresql import ENUM
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# Sources for the code: https://bitbucket.org/zzzeek/alembic/issue/67
|
||||
|
||||
def upgrade():
|
||||
''' Add the column merge_status to the table pull_requests.
|
||||
'''
|
||||
enum = ENUM('NO_CHANGE', 'FFORWARD', 'CONFLICTS', 'MERGE',
|
||||
name='merge_status_enum', create_type=False)
|
||||
enum.create(op.get_bind(), checkfirst=False)
|
||||
op.add_column(
|
||||
'pull_requests',
|
||||
sa.Column('merge_status', enum, nullable=True)
|
||||
)
|
||||
|
||||
def downgrade():
|
||||
''' Remove the column merge_status from the table pull_requests.
|
||||
'''
|
||||
ENUM(name="merge_status_enum").drop(op.get_bind(), checkfirst=False)
|
||||
op.drop_column('pull_requests', 'merge_status')
|
14
createdb.py
Normal file
|
@ -0,0 +1,14 @@
|
|||
#!/usr/bin/env python2
|
||||
|
||||
# These two lines are needed to run on EL6
|
||||
__requires__ = ['SQLAlchemy >= 0.8', 'jinja2 >= 2.4']
|
||||
import pkg_resources
|
||||
|
||||
from pagure import APP
|
||||
from pagure.lib import model
|
||||
|
||||
model.create_tables(
|
||||
APP.config['DB_URL'],
|
||||
APP.config.get('PATH_ALEMBIC_INI', None),
|
||||
acls=APP.config.get('ACLS', {}),
|
||||
debug=True)
|
153
doc/Makefile
Normal file
|
@ -0,0 +1,153 @@
|
|||
# Makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS =
|
||||
SPHINXBUILD = sphinx-build
|
||||
PAPER =
|
||||
BUILDDIR = _build
|
||||
|
||||
# Internal variables.
|
||||
PAPEROPT_a4 = -D latex_paper_size=a4
|
||||
PAPEROPT_letter = -D latex_paper_size=letter
|
||||
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||
# the i18n builder cannot share the environment and doctrees with the others
|
||||
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||
|
||||
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
|
||||
|
||||
help:
|
||||
@echo "Please use \`make <target>' where <target> is one of"
|
||||
@echo " html to make standalone HTML files"
|
||||
@echo " dirhtml to make HTML files named index.html in directories"
|
||||
@echo " singlehtml to make a single large HTML file"
|
||||
@echo " pickle to make pickle files"
|
||||
@echo " json to make JSON files"
|
||||
@echo " htmlhelp to make HTML files and a HTML help project"
|
||||
@echo " qthelp to make HTML files and a qthelp project"
|
||||
@echo " devhelp to make HTML files and a Devhelp project"
|
||||
@echo " epub to make an epub"
|
||||
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
|
||||
@echo " latexpdf to make LaTeX files and run them through pdflatex"
|
||||
@echo " text to make text files"
|
||||
@echo " man to make manual pages"
|
||||
@echo " texinfo to make Texinfo files"
|
||||
@echo " info to make Texinfo files and run them through makeinfo"
|
||||
@echo " gettext to make PO message catalogs"
|
||||
@echo " changes to make an overview of all changed/added/deprecated items"
|
||||
@echo " linkcheck to check all external links for integrity"
|
||||
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
|
||||
|
||||
clean:
|
||||
-rm -rf $(BUILDDIR)/*
|
||||
|
||||
html:
|
||||
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
||||
|
||||
dirhtml:
|
||||
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
|
||||
|
||||
singlehtml:
|
||||
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
|
||||
|
||||
pickle:
|
||||
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
|
||||
@echo
|
||||
@echo "Build finished; now you can process the pickle files."
|
||||
|
||||
json:
|
||||
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
|
||||
@echo
|
||||
@echo "Build finished; now you can process the JSON files."
|
||||
|
||||
htmlhelp:
|
||||
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run HTML Help Workshop with the" \
|
||||
".hhp project file in $(BUILDDIR)/htmlhelp."
|
||||
|
||||
qthelp:
|
||||
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
|
||||
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
|
||||
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/pagure.qhcp"
|
||||
@echo "To view the help file:"
|
||||
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/pagure.qhc"
|
||||
|
||||
devhelp:
|
||||
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
|
||||
@echo
|
||||
@echo "Build finished."
|
||||
@echo "To view the help file:"
|
||||
@echo "# mkdir -p $$HOME/.local/share/devhelp/pagure"
|
||||
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/pagure"
|
||||
@echo "# devhelp"
|
||||
|
||||
epub:
|
||||
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
|
||||
@echo
|
||||
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
|
||||
|
||||
latex:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo
|
||||
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
|
||||
@echo "Run \`make' in that directory to run these through (pdf)latex" \
|
||||
"(use \`make latexpdf' here to do that automatically)."
|
||||
|
||||
latexpdf:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo "Running LaTeX files through pdflatex..."
|
||||
$(MAKE) -C $(BUILDDIR)/latex all-pdf
|
||||
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
||||
|
||||
text:
|
||||
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
|
||||
@echo
|
||||
@echo "Build finished. The text files are in $(BUILDDIR)/text."
|
||||
|
||||
man:
|
||||
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
|
||||
@echo
|
||||
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
|
||||
|
||||
texinfo:
|
||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||
@echo
|
||||
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
|
||||
@echo "Run \`make' in that directory to run these through makeinfo" \
|
||||
"(use \`make info' here to do that automatically)."
|
||||
|
||||
info:
|
||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||
@echo "Running Texinfo files through makeinfo..."
|
||||
make -C $(BUILDDIR)/texinfo info
|
||||
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
|
||||
|
||||
gettext:
|
||||
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
|
||||
@echo
|
||||
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
|
||||
|
||||
changes:
|
||||
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
|
||||
@echo
|
||||
@echo "The overview file is in $(BUILDDIR)/changes."
|
||||
|
||||
linkcheck:
|
||||
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
|
||||
@echo
|
||||
@echo "Link check complete; look for any errors in the above output " \
|
||||
"or in $(BUILDDIR)/linkcheck/output.txt."
|
||||
|
||||
doctest:
|
||||
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
|
||||
@echo "Testing of doctests in the sources finished, look at the " \
|
||||
"results in $(BUILDDIR)/doctest/output.txt."
|
BIN
doc/_static/overview.png
vendored
Normal file
After Width: | Height: | Size: 38 KiB |
BIN
doc/_static/overview_simple.png
vendored
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
doc/_static/pagure.png
vendored
Normal file
After Width: | Height: | Size: 4.2 KiB |
16
doc/_static/site.css
vendored
Normal file
|
@ -0,0 +1,16 @@
|
|||
@import url("cloud.css");
|
||||
@import url("http://fonts.googleapis.com/css?family=Comfortaa");
|
||||
|
||||
.pagure-logo span {
|
||||
background: url("pagure.png") no-repeat scroll 50% 0 transparent;
|
||||
display: block;
|
||||
width: 134px;
|
||||
height: 64px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
h1.pagure-logo {
|
||||
font-family: 'Comfortaa', sans-serif;
|
||||
margin-top: -10px;
|
||||
margin-bottom: 12px;
|
||||
}
|
1
doc/_templates/pagure-logo.html
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
<h1 class='pagure-logo'><span></span>Pagure</h1>
|
5
doc/api.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
Pagure API
|
||||
==========
|
||||
|
||||
The API documentation can be found at https://pagure.io/api/0/ or within
|
||||
the sources in ``pagure/doc/api.rst``.
|
317
doc/conf.py
Normal file
|
@ -0,0 +1,317 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# pagure documentation build configuration file, created by
|
||||
# sphinx-quickstart on Wed Mar 1 10:30:13 2015.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its containing
|
||||
# dir.
|
||||
#
|
||||
# Note that not all possible configuration values are present in this
|
||||
# autogenerated file.
|
||||
#
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
pagurefile = os.path.join(
|
||||
os.path.dirname(__file__), '..', 'pagure', '__init__.py')
|
||||
|
||||
# Thanks to SQLAlchemy:
|
||||
# https://github.com/zzzeek/sqlalchemy/blob/master/setup.py#L104
|
||||
with open(pagurefile) as stream:
|
||||
VERSION = re.compile(
|
||||
r".*__version__ = '(.*?)'", re.S
|
||||
).match(stream.read()).group(1)
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another
|
||||
# directory, add these directories to sys.path here. If the directory is
|
||||
# relative to the documentation root, use os.path.abspath to make it
|
||||
# absolute, like shown here.
|
||||
#sys.path.insert(0, os.path.abspath('.'))
|
||||
|
||||
# -- General configuration ------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#needs_sphinx = '1.0'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
||||
extensions = [
|
||||
'sphinx.ext.autodoc', 'sphinx.ext.coverage', 'sphinx.ext.viewcode'
|
||||
]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# The suffix of source filenames.
|
||||
source_suffix = '.rst'
|
||||
|
||||
# The encoding of source files.
|
||||
#source_encoding = 'utf-8-sig'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = u'pagure'
|
||||
copyright = u'2015, Red Hat Inc, Pierre-Yves Chibon <pingou@pingoured.fr>'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement
|
||||
# for |version| and |release|, also used in various other places throughout
|
||||
# the built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
#version = __version__
|
||||
version = VERSION
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
#release = '1'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#language = None
|
||||
|
||||
# There are two options for replacing |today|: either, you set today to some
|
||||
# non-false value, then it is used:
|
||||
#today = ''
|
||||
# Else, today_fmt is used as the format for a strftime call.
|
||||
#today_fmt = '%B %d, %Y'
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
exclude_patterns = ['_build']
|
||||
|
||||
# The reST default role (used for this markup: `text`) to use for all
|
||||
# documents.
|
||||
#default_role = None
|
||||
|
||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||
#add_function_parentheses = True
|
||||
|
||||
# If true, the current module name will be prepended to all description
|
||||
# unit titles (such as .. function::).
|
||||
#add_module_names = True
|
||||
|
||||
# If true, sectionauthor and moduleauthor directives will be shown in the
|
||||
# output. They are ignored by default.
|
||||
#show_authors = False
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# A list of ignored prefixes for module index sorting.
|
||||
#modindex_common_prefix = []
|
||||
|
||||
|
||||
# -- Options for HTML output ----------------------------------------------
|
||||
|
||||
import cloud_sptheme as csp
|
||||
|
||||
html_style = 'site.css'
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
#html_theme = 'default'
|
||||
html_theme = "cloud"
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
#html_theme_options = {}
|
||||
|
||||
html_theme_options = {
|
||||
"sidebarwidth": "200px",
|
||||
"max_width": "900px",
|
||||
"compact_width": "800px",
|
||||
"minimal_width": "700px",
|
||||
|
||||
# Style it like Fedora..
|
||||
"bodyfont": "Cantarell",
|
||||
|
||||
"highlightcolor": "#79db32", # First Green
|
||||
|
||||
"sidebarbgcolor": "#FEFEFE",
|
||||
"sidebartrimcolor": "#FEFEFE",
|
||||
|
||||
"sectionbgcolor": "#FEFEFE",
|
||||
"sectiontrimcolor": "#FEFEFE",
|
||||
"sectiontextcolor": "#444444",
|
||||
|
||||
"relbarbgcolor": "#FEFEFE",
|
||||
"relbartextcolor": "#444444",
|
||||
"relbarlinkcolor": "#444444",
|
||||
|
||||
"bgcolor": "#FEFEFE",
|
||||
"textcolor": "#444444",
|
||||
#"linkcolor": "#79db32", # First Green
|
||||
"linkcolor": "#00009d",
|
||||
|
||||
"headtextcolor": "#444444",
|
||||
"headlinkcolor": "#444444",
|
||||
|
||||
#"codebgcolor"
|
||||
#"codetextcolor"
|
||||
"codetrimcolor": "#79db32", # First Green
|
||||
|
||||
"footerbgcolor": "#FEFEFE",
|
||||
}
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
#html_theme_path = []
|
||||
html_theme_path = [csp.get_theme_dir()]
|
||||
|
||||
# The name for this set of Sphinx documents. If None, it defaults to
|
||||
# "<project> v<release> documentation".
|
||||
#html_title = None
|
||||
|
||||
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||
#html_short_title = None
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top
|
||||
# of the sidebar.
|
||||
#html_logo = None
|
||||
|
||||
# The name of an image file (within the static path) to use as favicon of the
|
||||
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||
# pixels large.
|
||||
#html_favicon = None
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
|
||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||
# using the given strftime format.
|
||||
#html_last_updated_fmt = '%b %d, %Y'
|
||||
|
||||
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||
# typographically correct entities.
|
||||
#html_use_smartypants = True
|
||||
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
html_sidebars = {
|
||||
'**': [
|
||||
'pagure-logo.html',
|
||||
'localtoc.html',
|
||||
'relations.html',
|
||||
'sourcelink.html',
|
||||
'searchbox.html',
|
||||
]
|
||||
}
|
||||
|
||||
# Additional templates that should be rendered to pages, maps page names to
|
||||
# template names.
|
||||
#html_additional_pages = {}
|
||||
|
||||
# If false, no module index is generated.
|
||||
#html_domain_indices = True
|
||||
|
||||
# If false, no index is generated.
|
||||
#html_use_index = True
|
||||
|
||||
# If true, the index is split into individual pages for each letter.
|
||||
#html_split_index = False
|
||||
|
||||
# If true, links to the reST sources are added to the pages.
|
||||
#html_show_sourcelink = True
|
||||
|
||||
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
||||
#html_show_sphinx = True
|
||||
|
||||
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
||||
#html_show_copyright = True
|
||||
|
||||
# If true, an OpenSearch description file will be output, and all pages will
|
||||
# contain a <link> tag referring to it. The value of this option must be the
|
||||
# base URL from which the finished HTML is served.
|
||||
#html_use_opensearch = ''
|
||||
|
||||
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
||||
#html_file_suffix = None
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'pagure'
|
||||
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#'preamble': '',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title, author, documentclass
|
||||
# [howto/manual]).
|
||||
latex_documents = [
|
||||
(
|
||||
'index', 'pagure.tex', u'Pagure Documentation',
|
||||
u'Pierre-Yves Chibon \\textless{}pingou@pingoured.fr\\textgreater{}',
|
||||
'manual'
|
||||
),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
# the title page.
|
||||
#latex_logo = None
|
||||
|
||||
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||
# not chapters.
|
||||
#latex_use_parts = False
|
||||
|
||||
# If true, show page references after internal links.
|
||||
#latex_show_pagerefs = False
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#latex_show_urls = False
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#latex_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#latex_domain_indices = True
|
||||
|
||||
# -- Options for manual page output ---------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
(
|
||||
'index', 'pagure', u'Pagure Documentation',
|
||||
[u'Pierre-Yves Chibon <pingou@pingoured.fr>'],
|
||||
1
|
||||
)
|
||||
]
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#man_show_urls = False
|
||||
|
||||
|
||||
# -- Options for Texinfo output -------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
(
|
||||
'index', 'pagure', u'Pagure Documentation',
|
||||
u'Pierre-Yves Chibon <pingou@pingoured.fr>', 'pagure',
|
||||
'Small git-centric forge',
|
||||
'Miscellaneous'
|
||||
),
|
||||
]
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#texinfo_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#texinfo_domain_indices = True
|
||||
|
||||
# How to display URL addresses: 'footnote', 'no', or 'inline'.
|
||||
#texinfo_show_urls = 'footnote'
|
508
doc/configuration.rst
Normal file
|
@ -0,0 +1,508 @@
|
|||
Configuration
|
||||
=============
|
||||
|
||||
Pagure offers a wide varieties of options that must or can be used to
|
||||
adjust its behavior.
|
||||
|
||||
|
||||
Must options
|
||||
------------
|
||||
|
||||
Here are the options you must set up in order to get pagure running.
|
||||
|
||||
|
||||
SECRET_KEY
|
||||
~~~~~~~~~~
|
||||
|
||||
This key is used by flask to create the session. It should be kept secret
|
||||
and set as a long and random string.
|
||||
|
||||
|
||||
SALT_EMAIL
|
||||
~~~~~~~~~~
|
||||
|
||||
This key is used for when sending notification to ensure that when sending
|
||||
notifications to different users, each one of them has a different, unique
|
||||
and un-fakable ``Reply-To`` header that is then used by the milter to find
|
||||
out if the response received is a real one or a fake/invalid one.
|
||||
|
||||
|
||||
DB_URL
|
||||
~~~~~~
|
||||
|
||||
This key indicates to the framework how and where to connect to the database
|
||||
server. Pagure using `SQLAchemy <http://www.sqlalchemy.org/>`_ it can connect
|
||||
to a wide range of database server including MySQL, PostgreSQL and SQLite.
|
||||
|
||||
Examples values:
|
||||
|
||||
::
|
||||
|
||||
DB_URL=mysql://user:pass@host/db_name
|
||||
DB_URL=postgres://user:pass@host/db_name
|
||||
DB_URL = 'sqlite:////var/tmp/pagure_dev.sqlite'
|
||||
|
||||
Defaults to ``sqlite:////var/tmp/pagure_dev.sqlite``
|
||||
|
||||
|
||||
APP_URL
|
||||
~~~~~~~
|
||||
|
||||
This key indicates the URL at which this pagure instance will be made available.
|
||||
|
||||
Defaults to: ``https://pagure.org/``
|
||||
|
||||
|
||||
EMAIL_ERROR
|
||||
~~~~~~~~~~~
|
||||
|
||||
Pagure sends email when it caches an un-expected error (which saves you from
|
||||
having to monitor the logs regularly but if you like, the error is still
|
||||
present in the logs).
|
||||
This setting allows you to specify to which email address to send these error
|
||||
reports.
|
||||
|
||||
|
||||
GIT_URL_SSH
|
||||
~~~~~~~~~~~
|
||||
|
||||
This configuration key provides the information to the user on how to clone
|
||||
the git repos hosted on pagure via `SSH <https://en.wikipedia.org/wiki/Secure_Shell>`_.
|
||||
|
||||
The URL should end with a slash ``/``.
|
||||
|
||||
Defaults to: ``'ssh://git@pagure.org/'``
|
||||
|
||||
|
||||
GIT_URL_GIT
|
||||
~~~~~~~~~~~
|
||||
This configuration key provides the information to the user on how to clone
|
||||
the git repos hosted on pagure anonymously. This access can be granted via
|
||||
the ``git://`` or ``http(s)://`` protocols.
|
||||
|
||||
The URL should end with a slash ``/``.
|
||||
|
||||
Defaults to: ``'git://pagure.org/'``
|
||||
|
||||
|
||||
GIT_FOLDER
|
||||
~~~~~~~~~~
|
||||
|
||||
This configuration key points to where the folders containing the git repos
|
||||
of the projects are located.
|
||||
|
||||
Each project in pagure has 4 git repositories:
|
||||
|
||||
- the main repo for the code
|
||||
- the doc repo showed in the doc server
|
||||
- the ticket and request repos storing the metadata of the
|
||||
tickets/pull-requests
|
||||
|
||||
There are then another 2 folders specifying the locations of the forks and
|
||||
remote git repo used for the remotes pull-requests (ie: pull-request coming
|
||||
from a project not hosted on this instance of pagure).
|
||||
|
||||
|
||||
FORK_FOLDER
|
||||
~~~~~~~~~~~
|
||||
|
||||
This configuration key points to the folder where the git repos of forks of
|
||||
the projects are stored.
|
||||
|
||||
|
||||
DOCS_FOLDER
|
||||
~~~~~~~~~~~
|
||||
|
||||
This configuration key points to the folder where the git repos for the
|
||||
documentation of the projects are stored.
|
||||
|
||||
|
||||
TICKETS_FOLDER
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
This configuration key points to the folder where the git repos storing the
|
||||
metadata of the tickets opened against the project are stored .
|
||||
|
||||
|
||||
REQUESTS_FOLDER
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
This configuration key points to the folder where the git repos storing the
|
||||
metadata of the pull-requests opened against the project are stored.
|
||||
|
||||
|
||||
REMOTE_GIT_FOLDER
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
This configuration key points to the folder where the remote git repos (ie:
|
||||
not hosted on pagure) that someone used to open a pull-request against a
|
||||
project hosted on pagure are stored.
|
||||
|
||||
|
||||
SESSION_COOKIE_SECURE
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
When this is set to True, the session cookie will only be returned to the
|
||||
server via ssl (https). If you connect to the server via plain http, the
|
||||
cookie will not be sent. This prevents sniffing of the cookie contents.
|
||||
This may be set to False when testing your application but should always
|
||||
be set to True in production.
|
||||
|
||||
Defaults to: ``False`` for development, must be ``True`` in production with
|
||||
https.
|
||||
|
||||
|
||||
FROM_EMAIL
|
||||
~~~~~~~~~~
|
||||
|
||||
This setting allows to specify the email address used by this pagure instance
|
||||
when sending emails (notifications).
|
||||
|
||||
Defaults to: ``pagure@pagure.org``
|
||||
|
||||
|
||||
DOMAIN_EMAIL_NOTIFICATIONS
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This setting allows to specify the domain used by this pagure instance
|
||||
when sending emails (notifications). More precisely, this setting is used
|
||||
when building the ``msg-id`` header of the emails sent.
|
||||
|
||||
Defaults to: ``pagure.org``
|
||||
|
||||
|
||||
Configure Gitolite
|
||||
------------------
|
||||
|
||||
Pagure uses `gitolite <http://gitolite.com/>`_ as an authorization layer.
|
||||
Gitolite relies on `SSH <https://en.wikipedia.org/wiki/Secure_Shell>`_ for
|
||||
the authentication. In other words, SSH let you in and gitolite check if you
|
||||
are allowed to do what you are trying to do once you are inside.
|
||||
|
||||
|
||||
GITOLITE_HOME
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
This configuration key should point to the home of the user under which
|
||||
gitolite is ran.
|
||||
|
||||
|
||||
GITOLITE_VERSION
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
This configuration key allows to specify which version of gitolite you are
|
||||
using, it can be either ``2`` or ``3``.
|
||||
|
||||
Defaults to: ``3``.
|
||||
|
||||
|
||||
GITOLITE_KEYDIR
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
This configuration key points to the folder where gitolite stores and accesses
|
||||
the public SSH keys of all the user have access to the server.
|
||||
|
||||
Since pagure is the user interface, it is pagure that writes down the files
|
||||
in this directory effectively setting up the users to be able to use gitolite.
|
||||
|
||||
|
||||
GL_RC
|
||||
~~~~~
|
||||
|
||||
This configuration key must point to the file ``gitolite.rc`` used by gitolite
|
||||
to record who has access to what (ie: who has access to which repo/branch).
|
||||
|
||||
|
||||
GL_BINDIR
|
||||
~~~~~~~~~
|
||||
|
||||
This configuration key indicates the folder in which the gitolite tools can
|
||||
be found. It can be as simple as ``/usr/bin/`` if the tools have been installed
|
||||
using a package manager or something like ``/opt/bin/`` for a more custom
|
||||
install.
|
||||
|
||||
|
||||
EventSource options
|
||||
-------------------
|
||||
|
||||
EVENTSOURCE_SOURCE
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This configuration key indicates the URL at which the EventSource server is
|
||||
available. If not defined, pagure will behave as if there are no EventSource
|
||||
server running.
|
||||
|
||||
EVENTSOURCE_PORT
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
This configuration key indicates the port at which the EventSource server is
|
||||
running. This allows adjusting the port via the configuration file instead
|
||||
of hard-coding it in the code.
|
||||
|
||||
.. note:: The EventSource server requires a redis server (see ``Redis options``
|
||||
below)
|
||||
|
||||
|
||||
Web-hooks notifications
|
||||
-----------------------
|
||||
|
||||
WEBHOOK
|
||||
~~~~~~~
|
||||
|
||||
This configuration key allows turning on or off web-hooks notifications for
|
||||
this pagure instance.
|
||||
|
||||
Defaults to: ``False``.
|
||||
|
||||
.. note:: The Web-hooks server requires a redis server (see ``Redis options``
|
||||
below)
|
||||
|
||||
|
||||
Redis options
|
||||
-------------
|
||||
|
||||
REDIS_HOST
|
||||
~~~~~~~~~~
|
||||
|
||||
This configuration key indicates the host at which the `redis <http://redis.io/>`_
|
||||
server is running.
|
||||
|
||||
Defaults to: ``0.0.0.0``.
|
||||
|
||||
REDIS_PORT
|
||||
~~~~~~~~~~
|
||||
|
||||
This configuration key indicates the port at which the reds server can be
|
||||
contacted.
|
||||
|
||||
Defaults to: ``6379``.
|
||||
|
||||
REDIS_DB
|
||||
~~~~~~~~
|
||||
|
||||
This configuration key indicates the name of the redis database to use to
|
||||
communicate with the EventSource server.
|
||||
|
||||
Defaults to: ``0``.
|
||||
|
||||
|
||||
Authentication options
|
||||
----------------------
|
||||
|
||||
ADMIN_GROUP
|
||||
~~~~~~~~~~~
|
||||
|
||||
List of groups, local or remotes (if the openid server used supports the
|
||||
group extension), that are site admin. These admins can regenerate the
|
||||
gitolite configuration, the ssh key files, the hook-token for every project
|
||||
as well as manage users and groups.
|
||||
|
||||
|
||||
PAGURE_ADMIN_USERS
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
List of usernames that are site admin. These admins have the same rights as
|
||||
the user in the admin groups (listed above) as well as admin rights to
|
||||
every projects hosted on this pagure instance.
|
||||
|
||||
|
||||
Optional options
|
||||
----------------
|
||||
|
||||
SSH_KEYS
|
||||
~~~~~~~~
|
||||
|
||||
It is a good pratice to publish the fingerprint and public SSH key of a
|
||||
server you provide access to.
|
||||
Pagure offers the possibility to expose this information based on the values
|
||||
set in the configuration file, in the ``SSH_KEYS`` configuration key.
|
||||
|
||||
See the `SSH hostkeys/Fingerprints page on pagure.io <https://pagure.io/ssh_info>`_.
|
||||
|
||||
.. warning: The format is important
|
||||
|
||||
SSH_KEYS = {'RSA': {'fingerprint': '<foo>', 'pubkey': '<bar>'}}
|
||||
|
||||
Where `<foo>` and `<bar>` must be replaced by your values.
|
||||
|
||||
|
||||
ITEM_PER_PAGE
|
||||
~~~~~~~~~~~~~
|
||||
This configuration key allows you to configure the length of a page by
|
||||
setting the number of items on the page. Items can be commits, users, groups
|
||||
or projects for example.
|
||||
|
||||
Defaults to: ``50``.
|
||||
|
||||
|
||||
SMTP_SERVER
|
||||
~~~~~~~~~~~
|
||||
|
||||
This configuration key allows to configure the SMTP server to use when
|
||||
sending emails.
|
||||
|
||||
Defaults to: ``localhost``.
|
||||
|
||||
SMTP_PORT
|
||||
~~~~~~~~~
|
||||
|
||||
This configuration key allow to define the SMTP server port.
|
||||
|
||||
SMTP by default uses TCP port 25. The protocol for mail submission is
|
||||
the same, but uses port 587.
|
||||
SMTP connections secured by SSL, known as SMTPS, default to port 465
|
||||
(nonstandard, but sometimes used for legacy reasons).
|
||||
|
||||
Defaults to: ``25``
|
||||
|
||||
SMTP_SSL
|
||||
~~~~~~~~
|
||||
|
||||
This configuration key allows to specify whether the SMTP connections
|
||||
should secured over SSL
|
||||
|
||||
Defaults to: ``False``
|
||||
|
||||
SMTP_USERNAME
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
This configuration key allows usage of SMTP with auth
|
||||
|
||||
Note: Specify SMTP_USERNAME and SMTP_PASSWORD for using SMTP auth
|
||||
|
||||
Defaults to: ``None``
|
||||
|
||||
SMTP_PASSWORD
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
This configuration key allows usage of SMTP with auth
|
||||
|
||||
Note: Specify SMTP_USERNAME and SMTP_PASSWORD for using SMTP auth
|
||||
|
||||
Defaults to: ``None``
|
||||
|
||||
SHORT_LENGTH
|
||||
~~~~~~~~~~~~
|
||||
|
||||
This configuration key allows to configure the length of the commit ids or
|
||||
file hex displayed in the user interface.
|
||||
|
||||
Defaults to: ``6``.
|
||||
|
||||
|
||||
BLACKLISTED_PROJECTS
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This configuration key allows to set a list of project name that are forbidden.
|
||||
This list is used for example to avoid conflicts at the URL level between the
|
||||
static files located under ``/static/`` and a project that would be named
|
||||
``static`` and thus be located at ``/static``.
|
||||
|
||||
Defaults to:
|
||||
|
||||
::
|
||||
|
||||
[
|
||||
'static', 'pv', 'releases', 'new', 'api', 'settings',
|
||||
'logout', 'login', 'users', 'groups'
|
||||
]
|
||||
|
||||
|
||||
|
||||
CHECK_SESSION_IP
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
This configuration key allows to configure whether to check the user's IP
|
||||
address when retrieving its session. This makes things more secure but
|
||||
under certain setup it might not work (for example if there are proxies
|
||||
in front of the application).
|
||||
|
||||
Defaults to: ``True``.
|
||||
|
||||
|
||||
PAGURE_AUTH
|
||||
~~~~~~~~~~~~
|
||||
|
||||
This configuration key allows to specify which authentication method to use.
|
||||
Pagure supports currently two authentication methods, one relying on the
|
||||
Fedora Account System `FAS <https://admin.fedoraproject.org/accounts>`_,
|
||||
the other relying on local user accounts.
|
||||
It can therefore be either ``fas`` or ``local``.
|
||||
|
||||
Defaults to: ``fas``.
|
||||
|
||||
|
||||
IP_ALLOWED_INTERNAL
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This configuration key allows to specify which IP addresses are allowed
|
||||
to access the internal API endpoint. These endpoints are accessed by the
|
||||
milters for example and allow to perform action in the name of someone else.
|
||||
So they are sensitive, thus the check for the origin of the request using
|
||||
these endpoints.
|
||||
|
||||
Defaults to: ``['127.0.0.1', 'localhost', '::1']``.
|
||||
|
||||
|
||||
MAX_CONTENT_LENGTH
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This configuration key allows to specify the maximum size allowed when
|
||||
uploading content to pagure (for example, screenshots to a ticket).
|
||||
|
||||
Defaults to: ``4 * 1024 * 1024`` which corresponds to 4 megabytes.
|
||||
|
||||
|
||||
ENABLE_TICKETS
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
This configuration key allows to activate or de-activate the ticketing system
|
||||
for all the projects hosted on this pagure instance.
|
||||
|
||||
Defaults to: ``True``
|
||||
|
||||
|
||||
ENABLE_NEW_PROJECTS
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This configuration key allows to create or forbids creating new projects in
|
||||
the user interface of this pagure instance.
|
||||
|
||||
Defaults to: ``True``
|
||||
|
||||
|
||||
ENABLE_DEL_PROJECTS
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This configuration key allows to delete or forbids deleting projects in
|
||||
the user interface of this pagure instance.
|
||||
|
||||
Defaults to: ``True``
|
||||
|
||||
|
||||
EMAIL_SEND
|
||||
~~~~~~~~~~
|
||||
|
||||
This configuration key allows turning on or off all email notification for
|
||||
this pagure instance. This can be useful to turn off when developing on
|
||||
pagure, or for test or pre-production instances.
|
||||
|
||||
Defaults to: ``True``.
|
||||
|
||||
|
||||
OLD_VIEW_COMMIT_ENABLED
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
In version 1.3, pagure changed its URL scheme to view the commit of a
|
||||
project in order to add support for pseudo-namespaced projects.
|
||||
|
||||
For pagure instances older than 1.3, who care about backward compatibility,
|
||||
we added an endpoint ``view_commit_old`` that brings URL backward
|
||||
compatibility for URLs using the complete git hash (the 40 characters).
|
||||
For URLs using a shorter hash, the URLs will remain broken.
|
||||
|
||||
This configuration key allows turning on or off this backward compatibility
|
||||
which is useful for pagure instances running since before 1.3 but is not
|
||||
for newer instances.
|
||||
|
||||
Defaults to: ``False``.
|
27
doc/contributing.rst
Normal file
|
@ -0,0 +1,27 @@
|
|||
Contributing
|
||||
============
|
||||
|
||||
If you're submitting patches to pagure, please observe the following:
|
||||
|
||||
- Check that your python code is `PEP8-compliant
|
||||
<http://www.python.org/dev/peps/pep-0008/>`_. There is a `pep8 tool
|
||||
<http://pypi.python.org/pypi/pep8>`_ that can automatically check
|
||||
your source.
|
||||
|
||||
- Check that your code doesn't break the test suite. The test suite can be
|
||||
run using the ``runtests.sh`` shell script at the top of the sources.
|
||||
See :doc:`development` for more information about the test suite.
|
||||
|
||||
- If you are adding new code, please write tests for them in ``tests/``,
|
||||
the ``runtests.sh`` script will help you to see the coverage of your code
|
||||
in unit-tests.
|
||||
|
||||
- If your change warrants a modification to the docs in ``doc/`` or any
|
||||
docstrings in ``pagure/`` please make that modification.
|
||||
|
||||
.. note:: You have a doubt, you don't know how to do something, you have an
|
||||
idea but don't know how to implement it, you just have something bugging
|
||||
you?
|
||||
|
||||
Come to see us on IRC: ``#fedora-apps`` on irc.freenode.net or directly on
|
||||
`the project <http://pagure.io>`_.
|
75
doc/contributors.rst
Normal file
|
@ -0,0 +1,75 @@
|
|||
Contributors to pagure
|
||||
=========================
|
||||
|
||||
Pagure would be nothing without its contributors.
|
||||
|
||||
On July 27, 2016 (release 2.3.4), the list looks as follow:
|
||||
|
||||
================= ===========
|
||||
Number of commits Contributor
|
||||
================= ===========
|
||||
4107 Pierre-Yves Chibon <pingou@pingoured.fr>
|
||||
174 Ryan Lerch <rlerch@redhat.com>
|
||||
61 farhaanbukhsh <farhaan.bukhsh@gmail.com>
|
||||
59 Johan Cwiklinski <johan@x-tnd.be>
|
||||
48 Clement Verna <cverna@tutanota.com>
|
||||
36 Vivek Anand <vivekanand1101@gmail.com>
|
||||
18 Sayan Chowdhury <sayan.chowdhury2012@gmail.com>
|
||||
15 Gaurav Kumar <aavrug@gmail.com>
|
||||
15 Lubomír Sedlář <lsedlar@redhat.com>
|
||||
15 Patrick Uiterwijk <puiterwijk@redhat.com>
|
||||
15 Ralph Bean <rbean@redhat.com>
|
||||
13 Ghost-script <subho.prp@gmail.com>
|
||||
13 Mathieu Bridon <bochecha@fedoraproject.org>
|
||||
8 Lei Yang <yltt1234512@gmail.com>
|
||||
5 Mike McLean <mikem@redhat.com>
|
||||
5 Oliver Gutierrez <ogutierrez@redhat.com>
|
||||
5 vanzhiganov <vanzhiganov@ya.ru>
|
||||
5 yangl1996 <yltt1234512@gmail.com>
|
||||
4 Maciej Lasyk <maciek@lasyk.info>
|
||||
4 Paul W. Frields <stickster@gmail.com>
|
||||
3 Ankush Behl <cloudbehl@gmail.com>
|
||||
3 Anthony Lackey <alackey96@gmail.com>
|
||||
3 Dhriti Shikhar <dhriti.shikhar.rokz@gmail.com>
|
||||
3 Eric Barbour <ebarbour@redhat.com>
|
||||
3 Jan Pokorný <jpokorny@redhat.com>
|
||||
3 Kushal Khandelwal <kushal124@gmail.com>
|
||||
3 Pedro Lima <pedro.lima@gmail.com>
|
||||
2 Daniel Mach <dmach@redhat.com>
|
||||
2 Nuno Maltez <nuno@cognitiva.com>
|
||||
2 Richard Marko <rmarko@fedoraproject.org>
|
||||
2 Ricky Elrod <ricky@elrod.me>
|
||||
2 Simo Sorce <simo@redhat.com>
|
||||
2 Till Maas <opensource@till.name>
|
||||
2 bruno <bruno@wolff.to>
|
||||
2 dhrish20 <dhrish20@gmail.com>
|
||||
1 Anthony Lackey <alackey@localhost.localdomain>
|
||||
1 David Caro <dcaroest@redhat.com>
|
||||
1 Eric Barbour <emb4gu@virginia.edu>
|
||||
1 Kunaal Jain <kunaalus@gmail.com>
|
||||
1 Mathew Robinson <mathew.robinson3114@gmail.com>
|
||||
1 Pierre-YvesChibon <pingou@fedoraproject.org>
|
||||
1 Rahul Bajaj <rahulrb0509@gmail.com>
|
||||
1 Stanislav Ochotnicky <sochotnicky@redhat.com>
|
||||
1 Vyacheslav Anzhiganov <vanzhiganov@ya.ru>
|
||||
1 Yves Martin <ymartin1040@gmail.com>
|
||||
1 abhishek <abhishekarora12@gmail.com>
|
||||
1 jcvicelli <jcvicelli@gmail.com>
|
||||
1 pingou <pingou@fedoraproject.org>
|
||||
1 ryanlerch <rlerch@redhat.com>
|
||||
1 skrzepto <shims506@gmail.com>
|
||||
1 skrzepto <skrzepto@gmail.com>
|
||||
1 tenstormavi <avi.avinash3008@gmail.com>
|
||||
================= ===========
|
||||
|
||||
This list is generated using
|
||||
|
||||
::
|
||||
|
||||
git shortlog -s -n -e
|
||||
|
||||
|
||||
The old pagure logo has been created by ``Micah Denn <micah.denn@gmail.com>``,
|
||||
the new one, as well as the entire version 2 of the user interface (using
|
||||
bootstrap) is the work of ``Ryan Lerch <rlerch@redhat.com>`` many thanks
|
||||
to them for their work and understanding during the process.
|
261
doc/development.rst
Normal file
|
@ -0,0 +1,261 @@
|
|||
Development
|
||||
===========
|
||||
|
||||
Get the sources
|
||||
---------------
|
||||
|
||||
Anonymous:
|
||||
|
||||
::
|
||||
|
||||
git clone https://pagure.io/pagure.git
|
||||
|
||||
Contributors:
|
||||
|
||||
::
|
||||
|
||||
git clone ssh://git@pagure.io:pagure.git
|
||||
|
||||
|
||||
Dependencies
|
||||
------------
|
||||
|
||||
The dependencies of pagure are listed in the file ``requirements.txt``
|
||||
at the top level of the sources.
|
||||
|
||||
|
||||
.. note:: working in a `virtualenv <http://www.virtualenv.org/en/latest/>`_
|
||||
is tricky due to the dependency on `pygit2 <http://www.pygit2.org/>`_
|
||||
and thus on `libgit2 <https://libgit2.github.com/>`_
|
||||
but the pygit2 `documentation has a solution for this
|
||||
<http://www.pygit2.org/install.html#libgit2-within-a-virtual-environment>`_.
|
||||
|
||||
|
||||
Run pagure for development
|
||||
--------------------------
|
||||
Adjust the configuration file (secret key, database URL, admin group...)
|
||||
See :doc:`configuration` for more detailed information about the
|
||||
configuration.
|
||||
|
||||
|
||||
Create the database scheme::
|
||||
|
||||
./createdb
|
||||
|
||||
Create the folder that will receive the different git repositories:
|
||||
|
||||
::
|
||||
|
||||
mkdir {repos,docs,forks,tickets,requests,remotes}
|
||||
|
||||
|
||||
Run the server:
|
||||
|
||||
::
|
||||
|
||||
./runserver
|
||||
|
||||
If you want to change some configuration key you can create a file, place
|
||||
the configuration change in it and use it with
|
||||
|
||||
::
|
||||
|
||||
./runserver -c <config_file>
|
||||
|
||||
For example, create the file ``config`` with in it:
|
||||
|
||||
::
|
||||
|
||||
from datetime import timedelta
|
||||
# Makes the admin session longer
|
||||
ADMIN_SESSION_LIFETIME = timedelta(minutes=20000000)
|
||||
|
||||
# Use a postgresql database instead of sqlite
|
||||
DB_URL = 'postgresql://user:pass@localhost/pagure'
|
||||
# Change the OpenID endpoint
|
||||
FAS_OPENID_ENDPOINT = 'https://id.stg.fedoraproject.org'
|
||||
|
||||
APP_URL = '*'
|
||||
EVENTSOURCE_SOURCE = 'http://localhost:8080'
|
||||
EVENTSOURCE_PORT = '8080'
|
||||
DOC_APP_URL = '*'
|
||||
|
||||
# Avoid sending email when developping
|
||||
EMAIL_SEND = False
|
||||
|
||||
and run the server with:
|
||||
|
||||
::
|
||||
|
||||
./runserver -c config
|
||||
|
||||
To get some profiling information you can also run it as:
|
||||
|
||||
::
|
||||
|
||||
./runserver.py --profile
|
||||
|
||||
|
||||
You should be able to access the server at http://localhost:5000
|
||||
|
||||
|
||||
Every time you save a file, the project will be automatically restarted
|
||||
so you can see your change immediatly.
|
||||
|
||||
|
||||
|
||||
Create a pull-request for testing
|
||||
----------------------------------
|
||||
|
||||
When working on pagure, it is pretty often that one wanted to work on a
|
||||
feature or a bug related to pull-requests needs to create one.
|
||||
|
||||
Making a pull-request for development purposes isn't hard, if you remember
|
||||
that since you're running a local instance, the git repos created in your
|
||||
pagure instance are also local.
|
||||
|
||||
So here are in a few steps that one could perform to create a pull-request in a
|
||||
local pagure instance.
|
||||
|
||||
* Create a project on your pagure instance, let's say it will be called ``test``
|
||||
|
||||
* Create a folder ``clones`` somewhere in your system (you probably do not
|
||||
want it in the ``repos`` folder created above, next to it is fine though)::
|
||||
|
||||
mkdir clones
|
||||
|
||||
* Clone the repo of the ``test`` project into this ``clones`` folder::
|
||||
|
||||
cd clones
|
||||
git clone ~/path/to/pagure/repos/test.git
|
||||
|
||||
* Add and commit some files::
|
||||
|
||||
echo "*~" > .gitignore
|
||||
git add .gitignore
|
||||
git commit -m "Add a .gitignore file"
|
||||
echo "BSD" > LICENSE
|
||||
git add LICENSE
|
||||
git commit -m "Add a LICENSE file"
|
||||
|
||||
* Push these changes::
|
||||
|
||||
git push -u origin master
|
||||
|
||||
* Create a new branch and add a commit in it::
|
||||
|
||||
git branch new_branch
|
||||
git checkout new_branch
|
||||
touch test
|
||||
git add test
|
||||
git commit -m "Add file: test"
|
||||
|
||||
* Push this new branch::
|
||||
|
||||
git push -u origin new_branch
|
||||
|
||||
|
||||
Then go back to your pagure instance running in your web-browser, check the
|
||||
``test`` project. You should see two branches: ``master`` and ``new_branch``
|
||||
from there you should be able to open a new pull-request, either from the
|
||||
front page or via the ``File Pull Request`` button in the ``Pull Requests``
|
||||
page.
|
||||
|
||||
|
||||
|
||||
Coding standards
|
||||
----------------
|
||||
|
||||
We are trying to make the code `PEP8-compliant
|
||||
<http://www.python.org/dev/peps/pep-0008/>`_. There is a `pep8 tool
|
||||
<http://pypi.python.org/pypi/pep8>`_ that can automatically check
|
||||
your source.
|
||||
|
||||
|
||||
We are also inspecting the code using `pylint
|
||||
<http://pypi.python.org/pypi/pylint>`_ and aim of course for a 10/10 code
|
||||
(but it is an assymptotic goal).
|
||||
|
||||
.. note:: both pep8 and pylint are available in Fedora via yum:
|
||||
|
||||
::
|
||||
|
||||
yum install python-pep8 pylint
|
||||
|
||||
|
||||
Send patch
|
||||
----------
|
||||
|
||||
The easiest way to work on pagure is to make your own branch in git, make
|
||||
your changes to this branch, commit whenever you want, rebase on master,
|
||||
whenever you need and when you are done, send the patch either by email,
|
||||
via the trac or a pull-request (using git or github).
|
||||
|
||||
|
||||
The workflow would therefore be something like:
|
||||
|
||||
::
|
||||
|
||||
git branch <my_shiny_feature>
|
||||
git checkout <my_shiny_feature>
|
||||
<work>
|
||||
git commit file1 file2
|
||||
<more work>
|
||||
git commit file3 file4
|
||||
git checkout master
|
||||
git pull
|
||||
git checkout <my_shiny_feature>
|
||||
git rebase master
|
||||
git format-patch -2
|
||||
|
||||
This will create two patch files that you can send by email to submit in a ticket
|
||||
on pagure, by email or after forking the project on pagure by submitting a
|
||||
pull-request (in which case the last step above ``git format-patch -2`` is not
|
||||
needed.
|
||||
|
||||
|
||||
Unit-tests
|
||||
----------
|
||||
|
||||
Pagure has a number of unit-tests.
|
||||
|
||||
|
||||
We aim at having a full (100%) coverage of the whole code (including the
|
||||
Flask application) and of course a smart coverage as in we want to check
|
||||
that the functions work the way we want but also that they fail when we
|
||||
expect it and the way we expect it.
|
||||
|
||||
|
||||
Tests checking that function are failing when/how we want are as important
|
||||
as tests checking they work the way they are intended to.
|
||||
|
||||
``runtests.sh``, located at the top of the sources, helps to run the
|
||||
unit-tests of the project with coverage information using `python-nose
|
||||
<https://nose.readthedocs.org/>`_.
|
||||
|
||||
|
||||
.. note:: You can specify additional arguments to the nose command used
|
||||
in this script by just passing arguments to the script.
|
||||
|
||||
For example you can specify the ``-x`` / ``--stop`` argument:
|
||||
`Stop running tests after the first error or failure` by just doing
|
||||
|
||||
::
|
||||
|
||||
./runtests.sh --stop
|
||||
|
||||
|
||||
Each unit-tests files (located under ``tests/``) can be called
|
||||
by alone, allowing easier debugging of the tests. For example:
|
||||
|
||||
::
|
||||
|
||||
python tests/test_pragure_lib.py
|
||||
|
||||
|
||||
.. note:: In order to have coverage information you might have to install
|
||||
``python-coverage``
|
||||
|
||||
::
|
||||
|
||||
yum install python-coverage
|
51
doc/index.rst
Normal file
|
@ -0,0 +1,51 @@
|
|||
Pagure
|
||||
=======
|
||||
|
||||
Pagure is a light-weight git-centered forge based on pygit2.
|
||||
|
||||
Features:
|
||||
|
||||
* ``Open-sources``: Web-interface for the git repositories
|
||||
* ``Flexibility``: Ability to create any project you want
|
||||
* ``One place``: Keep your documentation and tickets in pagure
|
||||
* ``Collaboration``: Fork a project and make a pull-request
|
||||
* ``Integration``: Create pull-request from a fork hosted somewhere else than in
|
||||
pagure
|
||||
* ``Open data``: Sources, doc, ticket and pull-requests meta-data are available
|
||||
in the web interface but also in git repos which can thus be cloned and changed
|
||||
locally.
|
||||
* ``Freedom``: Pagure is fully Free and Open-Source Software!
|
||||
|
||||
|
||||
Resources:
|
||||
|
||||
- `Home page <https://pagure.io/>`_
|
||||
- `Git repository <http://pagure.io/pagure>`_
|
||||
- `Github mirror <https://github.com/pypingou/pagure>`_
|
||||
|
||||
|
||||
Contents:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
overview
|
||||
install
|
||||
install_milter
|
||||
install_evs
|
||||
install_webhooks
|
||||
configuration
|
||||
development
|
||||
usage
|
||||
contributing
|
||||
contributors
|
||||
|
||||
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
|
264
doc/install.rst
Normal file
|
@ -0,0 +1,264 @@
|
|||
Installing pagure
|
||||
=================
|
||||
|
||||
There are two ways to install pagure:
|
||||
|
||||
* via the RPM package (recommanded if you are using a RPM-based linux distribution)
|
||||
* via the setup.py
|
||||
|
||||
|
||||
|
||||
Installing pagure via RPM
|
||||
-------------------------
|
||||
|
||||
Here as well there are two ways of obtaining the RPM:
|
||||
|
||||
* From the main repositories
|
||||
|
||||
Pagure is packaged for Fedora since Fedora 21 and is available for RHEL and
|
||||
its derivative via the `EPEL repository <https://fedoraproject.org/wiki/EPEL>`.
|
||||
So installing it is as easy as:
|
||||
::
|
||||
|
||||
dnf install pagure pagure-milters pagure-ev pagure-webhook
|
||||
|
||||
or
|
||||
|
||||
::
|
||||
|
||||
yum install pagure pagure-milters pagure-ev pagure-webhook
|
||||
|
||||
The ``pagure`` package contains the core of the application and the doc server.
|
||||
(See the ``Overview`` page for a global overview of the structure of the
|
||||
project).
|
||||
|
||||
The ``pagure-milters`` package contains, as the name says, the milter (a
|
||||
mail filter to hook into a MTA).
|
||||
|
||||
The ``pagure-ev`` package contains the eventsource server.
|
||||
|
||||
The ``pagure-webhook`` package contains the web-hook server.
|
||||
|
||||
|
||||
.. note:: The last three packages are optional, pagure would work fine without
|
||||
them but the live-update, the webhook and the comment by email
|
||||
services will not work.
|
||||
|
||||
* From the sources
|
||||
|
||||
If you wish to run a newer version of pagure than what is in the repositories
|
||||
you can easily rebuild it as RPM.
|
||||
|
||||
Simply follow these steps:
|
||||
# Clone the sources::
|
||||
|
||||
git clone https://pagure.io/pagure.git
|
||||
|
||||
# Go to the folder::
|
||||
|
||||
cd pagure
|
||||
|
||||
# Build a tarball of the latest version of pagure::
|
||||
|
||||
python setup.py sdist
|
||||
|
||||
# Build the RPM::
|
||||
|
||||
rpmbuild -ta dist/pagure*.tar.gz
|
||||
|
||||
This will build pagure from the version present in your clone.
|
||||
|
||||
|
||||
Once, the RPM is installed the services ``pagure_milter`` and ``pagure_ev``
|
||||
are ready to be used but the database and the web-application parts still
|
||||
need to be configured.
|
||||
|
||||
|
||||
|
||||
Installing pagure via setup.py
|
||||
------------------------------
|
||||
|
||||
Pagure includes in its sources a ``setup.py`` automatint the installation
|
||||
of the web applications of pagure (ie: the core + the doc server).
|
||||
|
||||
|
||||
To install pagure via this mechanism simply follow these steps:
|
||||
# Clone the sources::
|
||||
|
||||
git clone https://pagure.io/pagure.git
|
||||
|
||||
# Go to the folder::
|
||||
|
||||
cd pagure
|
||||
|
||||
# Install the latest version of pagure::
|
||||
|
||||
python setup.py build
|
||||
sudo python setup.py install
|
||||
|
||||
.. note:: To install the eventsource server or the milter, refer to their
|
||||
respective documentations.
|
||||
|
||||
# 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`` |
|
||||
+------------------------------+------------------------------------------+
|
||||
|
||||
|
||||
|
||||
Set-up pagure
|
||||
-------------
|
||||
|
||||
Once pagure's files are installed, you still need to set up some things.
|
||||
|
||||
|
||||
* Create the folder release
|
||||
|
||||
This folder is used by project maintainers to upload the tarball of the
|
||||
releases of their project.
|
||||
|
||||
This folder must be accessible by the user under which the application is
|
||||
running (in our case: ``git``).
|
||||
::
|
||||
|
||||
mkdir -p /var/www/releases
|
||||
chown git:git /var/www/releases
|
||||
|
||||
|
||||
* Create the folders where the repos, forks and checkouts will be stored
|
||||
|
||||
Pagure stores the sources of a project in a git repo, offers a place to
|
||||
store the project's documentation in another repo, stores a JSON dump of all
|
||||
issues and of all pull-requests in another two repos, and keeps a local
|
||||
checkout of remote projects when asked to do remote pull-requests.
|
||||
All these repositories are stored in different folders that must be
|
||||
created manually.
|
||||
|
||||
For example you can place them under ``/srv/git/repositories/`` which would
|
||||
make ``/srv/git`` the home of your gitolite user.
|
||||
|
||||
You would then create the folders with:
|
||||
::
|
||||
|
||||
mkdir /srv/git/repositories/{docs,forks,tickets,requests,remotes}
|
||||
|
||||
|
||||
* Configure apache
|
||||
|
||||
If installed by RPM, you will find an example apache configuration file
|
||||
at: ``/etc/httpd/conf.d/pagure.conf``.
|
||||
|
||||
If not installed by RPM, the example files is present in the sources at:
|
||||
``files/pagure.conf``.
|
||||
|
||||
Adjust it for your needs.
|
||||
|
||||
|
||||
* Configure the WSGI file
|
||||
|
||||
If you installed by RPM, you will find an example WSGI file at:
|
||||
``/usr/share/pagure/pagure.wsgi`` and ``/usr/share/pagure/docs_pagure.wsgi``
|
||||
for the doc server.
|
||||
|
||||
If you did not install by RPM, these files are present in the sources at:
|
||||
``files/pagure.wsgi`` and ``files/doc_pagure.wsgi``.
|
||||
|
||||
Adjust them for your needs
|
||||
|
||||
|
||||
* Give apache permission to read the repositories owned by the ``git`` user.
|
||||
|
||||
For the sake of this document, we assume that the web application runs under
|
||||
the ``git`` user, the same user as your gitolite user, but apache itself
|
||||
runs under the ``httpd`` (or ``apache2``) user. So by default, apache
|
||||
will not be allowed to read git repositories created and managed by gitolite.
|
||||
|
||||
To give apache this permission (required to make git clone via http work),
|
||||
we use file access control lists (aka FACL):
|
||||
::
|
||||
|
||||
setfacl -m user:apache:rx --default
|
||||
setfacl -Rdm user:apache:rx /srv/git
|
||||
setfacl -Rm user:apache:rx /srv/git
|
||||
|
||||
Where ``/srv/git`` is the home of your gitolite user (which will thus need
|
||||
to be adjusted for your configuration).
|
||||
|
||||
|
||||
* Set up the configuration file of pagure
|
||||
|
||||
This is an important step which concerns the file ``/etc/pagure/pagure.cfg``.
|
||||
If you have installed pagure by RPM, this file is already there, otherwise
|
||||
you can find an example one in the sources at: ``files/pagure.cfg.sample``
|
||||
that you will have to copy to the right location.
|
||||
|
||||
Confer the ``Configuration`` section of this documentation for a full
|
||||
explanation of all the options of pagure.
|
||||
|
||||
* Create the database
|
||||
|
||||
You first need to create the database itself. For this, since pagure can
|
||||
work with: `PostgreSQL <http://www.postgresql.org/>`_,
|
||||
`MySQL <http://www.mysql.com/>`_ or `MariaDB <http://mariadb.org/>`_, we
|
||||
would like to invite you to consult the documentation of your database system
|
||||
for this operation.
|
||||
|
||||
Once you have specified in the configuration file the to url used to connect
|
||||
to the database, and create the database itself, you can now create the
|
||||
tables, the database scheme.
|
||||
|
||||
To create the database tables, you need to run the script
|
||||
``/usr/share/pagure/pagure_createdb.py`` and specify it the configuration
|
||||
file to use via an environment variable.
|
||||
|
||||
For example:
|
||||
::
|
||||
|
||||
PAGURE_CONFIG=/etc/pagure/pagure.cfg python /usr/share/pagure/pagure_createdb.py
|
||||
|
||||
This will tell ``/usr/share/pagure/pagure_createdb.py`` to use the database
|
||||
information specified in the file ``/etc/pagure/pagure.cfg``.
|
||||
|
||||
.. warning:: Pagure's default configuration is using sqlite. This is fine
|
||||
for development purpose but not for production use as sqlite does
|
||||
not support all the operations needed when updating the database
|
||||
schema. Do use PostgreSQL, MySQL or MariaDB in production.
|
||||
|
||||
* Stamp the alembic revision
|
||||
|
||||
For changes to existing tables, we rely on `Alembic <http://alembic.readthedocs.org/>`_.
|
||||
It uses `revisions` to perform the upgrades, but to know which upgrades are
|
||||
needed and which are already done, the current revision needs to be saved
|
||||
in the database. This will allow alembic to know apply the new revision when
|
||||
running it.
|
||||
|
||||
You can save the current revision in the database using the following command:
|
||||
::
|
||||
|
||||
cd /etc/pagure
|
||||
alembic stamp $(alembic heads |awk '{ print $1 }')
|
||||
|
||||
The ``cd /etc/pagure`` is needed as the command must be run in the folder
|
||||
where the file ``alembic.ini`` is. This file contains two important pieces
|
||||
of information:
|
||||
|
||||
* ``sqlalchemy.url`` which is the URL used to connect to the database, likely
|
||||
the same URL as the one in ``pagure.cfg``.
|
||||
* ``script_location`` which is the path to the ``versions`` folder containing
|
||||
all the alembic migration files.
|
||||
|
||||
The ``alembic stamp`` command is the one actually saving the current revision
|
||||
into the database. This current revision is found using ``alembic heads``
|
||||
which returns the most recent revision found by alembic, and since the
|
||||
database was just created, it is at the latest revision.
|
48
doc/install_evs.rst
Normal file
|
@ -0,0 +1,48 @@
|
|||
Installing pagure's EventSource server
|
||||
======================================
|
||||
|
||||
Eventsource or Server Sent Events are messages sent from a server to a web
|
||||
browser. It allows to refresh a page "live", ie, without the need to reload
|
||||
it entirely.
|
||||
|
||||
|
||||
Configure your system
|
||||
---------------------
|
||||
|
||||
The eventsource server is easy to set-up.
|
||||
|
||||
* Install the required dependencies
|
||||
|
||||
::
|
||||
|
||||
python-redis
|
||||
python-trollius
|
||||
python-trollius-redis
|
||||
|
||||
.. note:: We ship a systemd unit file for pagure_milter but we welcome patches
|
||||
for scripts for other init systems.
|
||||
|
||||
|
||||
* Install the files of the SSE server as follow:
|
||||
|
||||
+----------------------------------------+-----------------------------------------------------+
|
||||
| Source | Destination |
|
||||
+========================================+=====================================================+
|
||||
| ``ev-server/pagure-stream-server.py`` | ``/usr/libexec/pagure-ev/pagure-stream-server.py`` |
|
||||
+----------------------------------------+-----------------------------------------------------+
|
||||
| ``ev-server/pagure_ev.service`` | ``/etc/systemd/system/pagure_ev.service`` |
|
||||
+----------------------------------------+-----------------------------------------------------+
|
||||
|
||||
The first file is the script of the SSE server itself.
|
||||
|
||||
The second file is the systemd service file.
|
||||
|
||||
|
||||
* Finally, activate the service and ensure it's started upon boot:
|
||||
|
||||
::
|
||||
|
||||
systemctl enable redis
|
||||
systemctl start redis
|
||||
systemctl enable pagure_ev
|
||||
systemctl start pagure_ev
|
79
doc/install_milter.rst
Normal file
|
@ -0,0 +1,79 @@
|
|||
Installing pagure's milter
|
||||
==========================
|
||||
|
||||
A milter is a script that is ran by a Mail Transfer Agent (`MTA
|
||||
<https://en.wikipedia.org/wiki/Message_transfer_agent>`_)
|
||||
upon receiving an email via either a network or an unix socket.
|
||||
|
||||
If you want more information feel free to check out the corresponding page
|
||||
on wikipedia: `https://en.wikipedia.org/wiki/Milter
|
||||
<https://en.wikipedia.org/wiki/Milter>`_.
|
||||
|
||||
Configure your system
|
||||
---------------------
|
||||
|
||||
* Install the required dependencies
|
||||
|
||||
::
|
||||
|
||||
python-pymilter
|
||||
|
||||
.. note:: We ship a systemd unit file for pagure_milter but we welcome patches
|
||||
for scripts for other init systems.
|
||||
|
||||
.. note:: It also requires a MTA, we used postfix.
|
||||
|
||||
|
||||
* Create an alias ``reply``
|
||||
|
||||
This can be done in ``/etc/aliases``, for example:
|
||||
::
|
||||
|
||||
reply: /dev/null
|
||||
|
||||
|
||||
* Activate the ability of your MTA, to split users based on the character ``+``.
|
||||
This way all the emails sent to ``reply+...@example.com`` will be forwarded
|
||||
to your alias for ``reply``.
|
||||
|
||||
|
||||
In postfix this is done via:
|
||||
::
|
||||
|
||||
recipient_delimiter = +
|
||||
|
||||
* Hook the milter in the MTA
|
||||
|
||||
In postfix this is done via:
|
||||
::
|
||||
|
||||
non_smtpd_milters = unix:/var/run/pagure/paguresock
|
||||
smtpd_milters = unix:/var/run/pagure/paguresock
|
||||
|
||||
|
||||
* Install the files of the milter as follow:
|
||||
|
||||
+--------------------------------------+---------------------------------------------------+
|
||||
| Source | Destination |
|
||||
+======================================+===================================================+
|
||||
| ``milters/comment_email_milter.py`` | ``/usr/share//pagure/comment_email_milter.py`` |
|
||||
+--------------------------------------+---------------------------------------------------+
|
||||
| ``milters/milter_tempfile.conf`` | ``/usr/lib/tmpfiles.d/pagure-milter.conf`` |
|
||||
+--------------------------------------+---------------------------------------------------+
|
||||
| ``milters/pagure_milter.service`` | ``/etc/systemd/system/pagure_milter.service`` |
|
||||
+--------------------------------------+---------------------------------------------------+
|
||||
|
||||
The first file is the script of the milter itself.
|
||||
|
||||
The second file is a file specific for systemd and ensuring the temporary
|
||||
folders needed by the milter are re-created if needed at each boot.
|
||||
|
||||
The third file is the systemd service file.
|
||||
|
||||
|
||||
* Activate the service and ensure it's started upon boot:
|
||||
|
||||
::
|
||||
|
||||
systemctl enable pagure_milter
|
||||
systemctl start pagure_milter
|
49
doc/install_webhooks.rst
Normal file
|
@ -0,0 +1,49 @@
|
|||
Installing pagure's web-hooks notification system
|
||||
=================================================
|
||||
|
||||
Web-hooks are a notification system upon which a system makes a http POST
|
||||
request with some data upon doing an action. This allows notifying a system
|
||||
that an action has occured.
|
||||
|
||||
If you want more information feel free to check out the corresponding page
|
||||
on wikipedia: `https://en.wikipedia.org/wiki/Webhook
|
||||
<https://en.wikipedia.org/wiki/Webhook>`_.
|
||||
|
||||
Configure your system
|
||||
---------------------
|
||||
|
||||
* Install the required dependencies
|
||||
|
||||
::
|
||||
|
||||
python-redis
|
||||
python-trollius
|
||||
python-trollius-redis
|
||||
|
||||
.. note:: We ship a systemd unit file for pagure_webhook but we welcome patches
|
||||
for scripts for other init systems.
|
||||
|
||||
|
||||
* Install the files of the web-hook server as follow:
|
||||
|
||||
+----------------------------------------------+----------------------------------------------------------+
|
||||
| Source | Destination |
|
||||
+==============================================+==========================================================+
|
||||
| ``webhook-server/pagure-webhook-server.py`` | ``/usr/libexec/pagure-webhook/pagure-webhook-server.py`` |
|
||||
+----------------------------------------------+----------------------------------------------------------+
|
||||
| ``webhook-server/pagure_webhook.service`` | ``/etc/systemd/system/pagure_webhook.service`` |
|
||||
+----------------------------------------------+----------------------------------------------------------+
|
||||
|
||||
The first file is the script of the web-hook server itself.
|
||||
|
||||
The second file is the systemd service file.
|
||||
|
||||
|
||||
* Activate the service and ensure it's started upon boot:
|
||||
|
||||
::
|
||||
|
||||
systemctl enable redis
|
||||
systemctl start redis
|
||||
systemctl enable pagure_webhook
|
||||
systemctl start pagure_webhook
|
62
doc/milter.rst
Normal file
|
@ -0,0 +1,62 @@
|
|||
Pagure's Milter
|
||||
===============
|
||||
|
||||
`Milter <http://www.postfix.org/MILTER_README.html>`_ are script executed by
|
||||
postfix upon sending or receiving an email.
|
||||
|
||||
We use this system to allow pagure's users to comment on a ticket (or a
|
||||
pull-request) by directly replying to the email sent as a notification.
|
||||
|
||||
Pagure's milter is designed to be run on the same machine as the mail server
|
||||
(postfix by default). Postfix connecting to the milter via a unix socket.
|
||||
|
||||
The milter itself is a service managed by systemd.
|
||||
You can find all the relevant files for the milter under the ``milters`` folder
|
||||
in the sources.
|
||||
|
||||
|
||||
Install the milter
|
||||
------------------
|
||||
|
||||
The first step to enable the milter on a pagure instance is thus to install the
|
||||
``.service`` file for systemd and place the corresponding script that, by
|
||||
default, should go to ``/usr/share/pagure/comment_email_milter.py``.
|
||||
|
||||
If you are using the RPM, install ``pagure-milters`` should provide and install
|
||||
all the files correctly.
|
||||
|
||||
|
||||
Activate the milter
|
||||
-------------------
|
||||
|
||||
Make sure the milter is running and will be automaticall started at boot by
|
||||
running the commands:
|
||||
|
||||
To start the milter:
|
||||
|
||||
::
|
||||
|
||||
systemctl start pagure_milter
|
||||
|
||||
To ensure the milter is always started at boot time:
|
||||
|
||||
::
|
||||
|
||||
systemctl enable pagure_milter
|
||||
|
||||
|
||||
Activate the milter in postfix
|
||||
------------------------------
|
||||
|
||||
To actually activate the milter in postfix is in fact really easy, all it takes
|
||||
is two lines in the ``main.cf`` file of postfix:
|
||||
|
||||
::
|
||||
|
||||
non_smtpd_milters = unix:/var/run/pagure/paguresock
|
||||
smtpd_milters = unix:/var/run/pagure/paguresock
|
||||
|
||||
These two lines are pointing to the unix socket used by postfix to communicate
|
||||
with the milter. This socket is defined in the milter file itself, in the
|
||||
sources: ``milters/comment_email_milter.py``.
|
||||
|
53
doc/overview.ascii
Normal file
|
@ -0,0 +1,53 @@
|
|||
Grants/Denies access
|
||||
+------------+ +-----------+
|
||||
| | | |
|
||||
User's git actions+---------------------->| Gitolite +-------------------------->+ Git repos |
|
||||
| | | |
|
||||
+-----+------+ +-----------+
|
||||
^
|
||||
|
|
||||
|
|
||||
+------------------------------------------+
|
||||
|
|
||||
|
|
||||
+-------------------------+ |
|
||||
Notifications | | |
|
||||
+------------------------------------+ Postfix |<--------------------------------+
|
||||
| | | | |
|
||||
| | +-------------------+ | |
|
||||
| | | | | |
|
||||
v | | Pagure's milter | | |
|
||||
User's mail client | | +--------------+ | |
|
||||
+ +-----+--------+----------+ | | |
|
||||
| ^ Updates | | |
|
||||
| | | | |
|
||||
| Replies | | | |
|
||||
+---------------------------------------------------+ | | |
|
||||
| | |
|
||||
| | |
|
||||
+--------------+ | | |
|
||||
| | | | |
|
||||
+----------------------->| Pagure | v | |
|
||||
| | Doc server | +------------+-+ |
|
||||
| | | |{s} | |
|
||||
| +--------------+ +------->| Database | |
|
||||
| | | | |
|
||||
User's web browser--+ http requests Updates | +--------------+ |
|
||||
^ | & queries| |
|
||||
| | | |
|
||||
| | +--------------+ | |
|
||||
| | | +----------+---------------------------------+
|
||||
| +----------------------->| Pagure |
|
||||
| | web server +---+ +----------------------+ +----------------+
|
||||
| | | | | | | |
|
||||
| +--------------+ | | Pagure | | Third Party |
|
||||
| +---------->| Web hooks' server +-------------->| Services |
|
||||
| | | | | |
|
||||
| redis | +----------------------+ +----------------+
|
||||
| |
|
||||
| | +----------------------+
|
||||
| +---------->| |
|
||||
| | Pagure |
|
||||
+----------------------------------------------------------------------+ EventSource server |
|
||||
Server-Sent Event | |
|
||||
+----------------------+
|
104
doc/overview.rst
Normal file
|
@ -0,0 +1,104 @@
|
|||
Overview
|
||||
========
|
||||
|
||||
Pagure is split over multiple components, each having their purpose and all
|
||||
but one (the core application) being optional.
|
||||
|
||||
These components are:
|
||||
|
||||
.. contents::
|
||||
|
||||
|
||||
Before going into the overall picture, one should realize that most of the
|
||||
components listed above are optional.
|
||||
|
||||
Here is a diagram representing pagure without all the optionals components:
|
||||
|
||||
.. image:: _static/overview_simple.png
|
||||
:target: _static/overview_simple.png
|
||||
|
||||
|
||||
And here is a diagram of all the components together:
|
||||
|
||||
.. image:: _static/overview.png
|
||||
:target: _static/overview.png
|
||||
|
||||
Pagure core application
|
||||
-----------------------
|
||||
|
||||
The core application is the flask application interacting with gitolite to
|
||||
provide a web UI to the git repositories as well as tickets and pull-requests.
|
||||
This is the main application for the forge.
|
||||
|
||||
|
||||
Gitolite
|
||||
--------
|
||||
|
||||
Currently pagure uses `gitolite <http://gitolite.com/gitolite/index.html>`_
|
||||
to grant or deny `ssh <https://en.wikipedia.org/wiki/Secure_Shell>`_ access
|
||||
to the git repositories, in other words to grant or deny read and/or write
|
||||
access to the git repositories.
|
||||
|
||||
Pagure supports cloning over both ssh and http, but writing can only be done
|
||||
via ssh, through gitolite.
|
||||
|
||||
|
||||
Pagure doc server
|
||||
-----------------
|
||||
|
||||
While integrated into the main application at first, it has been split out
|
||||
for security concern, displaying information directly provided by the user
|
||||
without a clear/safe way of filtering for un-safe script or hacks is a
|
||||
security hole.
|
||||
For this reason we also strongly encourage anyone wanting to deploy their
|
||||
own instance of pagure with the doc server, to run this application on a
|
||||
completely different domain name (not just a sub-domain) in order to reduce
|
||||
the cross-site forgery risks.
|
||||
|
||||
Pagure can be run just fine without the doc server, all you need to do is to
|
||||
**not** define the variable ``DOC_APP_URL`` in the configuration file.
|
||||
|
||||
|
||||
Pagure milter
|
||||
-------------
|
||||
|
||||
The milter is a script, receiving an email as input and performing an action
|
||||
with it.
|
||||
|
||||
In the case of pagure, the milter is used to allow replying on a comment
|
||||
of a ticket or a pull-request by directly replying to the notification sent.
|
||||
No need to go to the page anymore to reply to a comment someone made.
|
||||
|
||||
The milter integrates with a MTA such as postfix or sendmail that you will
|
||||
have running and have access to in order to change its configuration.
|
||||
|
||||
|
||||
Pagure EventSource Server
|
||||
-------------------------
|
||||
|
||||
Eventsource or Server Sent Events are messages sent from a server to a browser.
|
||||
|
||||
For pagure this technology is used to allow live-refreshing of a page when
|
||||
someone is viewing it. For example, while you are reading a ticket if someone
|
||||
comments on it, the comment will automatically show up on the page without
|
||||
the need for you to reload the entire page.
|
||||
|
||||
The flow is: the main pagure server does an action, sends a message over
|
||||
redis, the eventsource server picks it up and send it to the browsers waiting
|
||||
for it, then javascript code is executed to refresh the page based on the
|
||||
information received.
|
||||
|
||||
|
||||
Pagure web-hook Server
|
||||
-------------------------
|
||||
|
||||
Sends notifications to third party services using POST http requests.
|
||||
|
||||
This is the second notifications system in pagure with `fedmsg <http://fedmsg.com/>`_.
|
||||
These notifications are running on their own service to prevent blocking the
|
||||
main web application in case the third part service is timing-out or just
|
||||
being slow.
|
||||
|
||||
The flow is: the main pagure server does an action, sends a message over
|
||||
redis, the web-hook server picks it up, build the query and performs the
|
||||
POST request to the specified URLs.
|
26
doc/overview_simple.ascii
Normal file
|
@ -0,0 +1,26 @@
|
|||
|
||||
Grants/Denies access
|
||||
+------------+ +-----------+
|
||||
|cfffca4 | | |
|
||||
User's git actions+--------------------->+ Gitolite +-------------------------->+ Git repos |
|
||||
| | | |
|
||||
+------------+ +-----------+
|
||||
^
|
||||
|
|
||||
+-----------------------------+
|
||||
|
|
||||
User's mail client |
|
||||
^ +---------------+ |
|
||||
| Notifications | | |
|
||||
+----------------------------------+ Mail server | |
|
||||
| | |
|
||||
+---------------+ |
|
||||
^ |
|
||||
| |
|
||||
| |
|
||||
+--------------+ Updates +--------------+
|
||||
| | & queries |{s} |
|
||||
User's web browser+---------------------->+ Pagure +------------->| Database |
|
||||
| web server | | |
|
||||
| | +--------------+
|
||||
+--------------+
|
2
doc/requirements.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
sphinx
|
||||
cloud_sptheme
|
43
doc/usage.rst
Normal file
|
@ -0,0 +1,43 @@
|
|||
Usage
|
||||
=====
|
||||
|
||||
Using pagure should come fairly easily, especially to people already used
|
||||
to forges such as GitHub or GitLab. There are however some tips and tricks
|
||||
which can be useful to know and that this section of the doc covers.
|
||||
|
||||
|
||||
One of the major difference with GitHub and GitLab is that for each project
|
||||
on pagure, four git repositories are made available to the admins of the
|
||||
project:
|
||||
|
||||
* A git repository containing the source code, displayed in the main section
|
||||
of the pagure project.
|
||||
* A git repository for the documentation
|
||||
* A git repository for the issues and their metadata
|
||||
* A git repository for the metadata for pull-requests
|
||||
|
||||
|
||||
You can find the URLs to access or clone these git repositories on the
|
||||
overview page of the project. On the menu on the right side, there is a menu
|
||||
`Source GIT URLs`, next to it is a little `more` button, by clicking on it
|
||||
you will be given the URLs to the other three git repos.
|
||||
|
||||
Each section correspond to one of the four git repositories created for each
|
||||
project.
|
||||
|
||||
|
||||
|
||||
Contents:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
usage/first_steps
|
||||
usage/project_settings
|
||||
usage/roadmap
|
||||
usage/using_doc
|
||||
usage/using_webhooks
|
||||
usage/ticket_templates
|
||||
usage/pr_custom_page
|
||||
usage/theming
|
||||
usage/upgrade_db
|
BIN
doc/usage/_static/pagure_custom_pr.png
Normal file
After Width: | Height: | Size: 66 KiB |
BIN
doc/usage/_static/pagure_my_settings.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
doc/usage/_static/pagure_roadmap2.png
Normal file
After Width: | Height: | Size: 63 KiB |
BIN
doc/usage/_static/pagure_ticket_template.png
Normal file
After Width: | Height: | Size: 49 KiB |
80
doc/usage/first_steps.rst
Normal file
|
@ -0,0 +1,80 @@
|
|||
First Steps on pagure
|
||||
=====================
|
||||
|
||||
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
|
||||
--------------------------------------
|
||||
|
||||
Pagure has its own user account system.
|
||||
|
||||
For instances of pagure such as the one at `pagure.io <https://pagure.io>`_
|
||||
where the authentication is delegated to a third party (in the case of
|
||||
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
|
||||
the first time as the system will simply not know you.
|
||||
|
||||
If you run your own pagure instance which uses the local authentication
|
||||
system, then you will find on the login page an option to create a new
|
||||
account.
|
||||
|
||||
|
||||
Upload your SSH key
|
||||
-------------------
|
||||
|
||||
Pagure uses gitolite to manage who has read/write access to which git
|
||||
repository via `ssh <https://en.wikipedia.org/wiki/Secure_Shell>`_.
|
||||
|
||||
An ssh key is composed of two parts:
|
||||
|
||||
* a private key, which you must keep to yourself and never share with anyone.
|
||||
* a public key, which is public and therefore can be shared with anyone.
|
||||
|
||||
If you have never generated a ssh key, you can do so by running:
|
||||
|
||||
::
|
||||
|
||||
ssh-keygen
|
||||
|
||||
or alternatively on GNOME using the application ``seahorse``.
|
||||
|
||||
This will create two files in ``~/.ssh/`` (``~`` is the symbol for your home
|
||||
folder).
|
||||
|
||||
These two files will be named (for example) ``id_rsa`` and ``id_rsa.pub``.
|
||||
The first one is the private key that must never be shared. The second is
|
||||
the public key that can be uploaded on pagure to give you ssh access.
|
||||
|
||||
To upload your public key onto pagure, login and click on the user icon on
|
||||
the top right corner, there, select ``My settings``.
|
||||
|
||||
.. image:: _static/pagure_my_settings.png
|
||||
:target: _static/pagure_my_settings.png
|
||||
|
||||
|
||||
Configure the default email address
|
||||
-----------------------------------
|
||||
|
||||
If the pagure instance you use is using local user authentication, then when
|
||||
you created your account you could choose whichever email address you prefer
|
||||
to use, but in the case (like pagure.io) where the pagure instance relies
|
||||
on an external authentication service, the email address provided by this
|
||||
service may be different from the one you prefer.
|
||||
|
||||
Your settings' page (cf the image above for how to access to the page) allow
|
||||
you to add multiple email address and set one as default.
|
||||
|
||||
Your default email address is the address that will be used to send you
|
||||
notifications and also as the email address in the git commit if you merge
|
||||
a pull-request with a merge commit.
|
||||
|
||||
For online editing, when doing the commit, you will be presented with the
|
||||
list of valid email addresses associated with your account and you will be
|
||||
able to choose which one you wish to use.
|
||||
|
||||
.. note:: All email address will need to be confirmed to be activated, this
|
||||
is done via a link sent by email to the address. If you do not
|
||||
receive this link, don't forget to check your spam folder!
|
74
doc/usage/pr_custom_page.rst
Normal file
|
@ -0,0 +1,74 @@
|
|||
Customize the PR page
|
||||
=====================
|
||||
|
||||
Pagure offers the possibility to customize the page that creates pull-request
|
||||
to add your specific information, such as: please follow the XYZ coding style,
|
||||
run the tests or whatever you wish to inform contributors when they open a
|
||||
new pull-request.
|
||||
|
||||
The customization is done via a file in the git repository containing the
|
||||
meta-data for the pull-requests. This file must be placed under a ``templates``
|
||||
folder, be named ``contributing.md`` and can be formatted as you wish using
|
||||
markdown.
|
||||
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
For a project named ``test`` on ``pagure.io``.
|
||||
|
||||
* First, clone the pull-request git repo [#f1]_ and move into it
|
||||
|
||||
::
|
||||
|
||||
git clone ssh://git@pagure.io/requests/test.git
|
||||
cd test
|
||||
|
||||
* Create the templates folder
|
||||
|
||||
::
|
||||
|
||||
mkdir templates
|
||||
|
||||
* Create the customized PR info
|
||||
|
||||
::
|
||||
|
||||
vim templates/contributing.md
|
||||
|
||||
And place in this file the following content:
|
||||
|
||||
::
|
||||
|
||||
Contributing to test
|
||||
====================
|
||||
|
||||
When creating a pull-request against test, there are couple of items to do
|
||||
that will speed up the review process:
|
||||
|
||||
* Ensure the unit-tests are all passing (cf the ``runtests.sh`` script at the
|
||||
top level of the sources)
|
||||
* Check if your changes are [pep8](https://www.python.org/dev/peps/pep-0008/)
|
||||
compliant for this you can install ``python-pep8`` and run the ``pep8`` CLI
|
||||
tool
|
||||
|
||||
|
||||
* Commit and push the changes to the git repo
|
||||
|
||||
::
|
||||
|
||||
git add templates
|
||||
git commit -m "Customize the PR page"
|
||||
git push
|
||||
|
||||
|
||||
* And this is how it will look like
|
||||
|
||||
.. image:: _static/pagure_custom_pr.png
|
||||
:target: _static/pagure_custom_pr.png
|
||||
|
||||
|
||||
|
||||
.. [#f1] All the URLs to the different git repositories can be found on the
|
||||
main page of the project, on the right-side menu, under the section
|
||||
``Source GIT URLs``, click on ``more`` to see them.
|
145
doc/usage/project_settings.rst
Normal file
|
@ -0,0 +1,145 @@
|
|||
Project settings
|
||||
================
|
||||
|
||||
Each project have a number of options that can be tweaked in the settings
|
||||
page of the project which is accessible to the person having full commits
|
||||
to the project.
|
||||
|
||||
This page presents the different settings and there effect.
|
||||
|
||||
|
||||
`Activate always merge`
|
||||
------------------------
|
||||
|
||||
This boolean enables or disables always making a merge commit when merging
|
||||
a pull-request.
|
||||
|
||||
When merging a pull-request in pagure there are three states:
|
||||
|
||||
* fast-forward: when the commits in the pull-request can be fast-forwarded
|
||||
pagure signals it and just fast-forward the commit, keeping the history linear.
|
||||
* merge: when the commits in the pull-request cannot be merged without a merge
|
||||
commit, pagure signals it and performs this merge commit.
|
||||
* conflicts: when the commits in the pull-request cannot be merged at all
|
||||
automatically due to one or more conflicts. Then pagure signals it and prevent
|
||||
merging.
|
||||
|
||||
If the `Activate always merge` option is on, then the `fast-forward` option
|
||||
above is disabled in favor of the `merge` option.
|
||||
|
||||
|
||||
`Activate comment editing`
|
||||
--------------------------
|
||||
|
||||
This boolean enables or disables editing comments.
|
||||
|
||||
After commenting on a ticket or a pull-request, the admins of the project
|
||||
and the author of the comment may be allowed to edit the comment.
|
||||
This allows them to adjust the wording or the style as they wish.
|
||||
|
||||
.. note:: notification about a comment is only sent once with the original
|
||||
text, changes performed later will not trigger a new notification.
|
||||
|
||||
Some project may not want to allow editing comments after they were posted
|
||||
and this setting allows turning it on or off.
|
||||
|
||||
|
||||
`Activate Enforce signed-off commits in pull-request`
|
||||
-----------------------------------------------------
|
||||
|
||||
This boolean enables or disables checking for a 'Signed-off-by' line (case
|
||||
insensitive) in the commit messages of the pull-requests.
|
||||
|
||||
If this line is missing, pagure will display a message near the `Merge`
|
||||
button, allowing project admin to request the PR to be updated.
|
||||
|
||||
.. note:: This setting does not prevent commits without this 'signed-off-by'
|
||||
line to be pushed directly, it only work at the pull-request level.
|
||||
|
||||
|
||||
`Activate issue tracker`
|
||||
------------------------
|
||||
|
||||
This boolean simply enables or disables the issue tracker for the project.
|
||||
So if you are tracking your ticket on a different system, you can simply
|
||||
disable reporting issue on pagure by un-checking this option.
|
||||
|
||||
|
||||
`Activate Minimum score to merge pull-request`
|
||||
----------------------------------------------
|
||||
|
||||
This option can be used for project wishing to enforce having a minimum
|
||||
number of people reviewing a pull-request before it can be merged.
|
||||
|
||||
If this option is enabled, anyone can vote in favor or against a pull-request
|
||||
and the sum of the votes in favor minus the sum of the votes againsts give
|
||||
the pull-request a score that should be equal or great to the value
|
||||
entered in this option for the pull-request to be allowed to be merged.
|
||||
|
||||
.. note:: Only the main comments (ie: not in-line) are taken into account
|
||||
to calculate the score of the pull-request.
|
||||
|
||||
To vote in favor of a pull-request, use either:
|
||||
* ``+1``
|
||||
* ``:thumbsup:``
|
||||
|
||||
To vote against a pull-request, use either:
|
||||
* ``-1``
|
||||
* ``:thumbsdown:``
|
||||
|
||||
.. note:: Pull-Request reaching the minimum score are not automatically merged
|
||||
|
||||
.. note:: Anyone can vote on the pull-request, not only the contributors.
|
||||
|
||||
|
||||
`Activate Only assignee can merge pull-request`
|
||||
-----------------------------------------------
|
||||
|
||||
This option can be used for project wishing to institute a strong review
|
||||
workflow where pull-request are first assigned then merged.
|
||||
|
||||
If this option is enabled, only the person assigned to the pull-request
|
||||
can merge it.
|
||||
|
||||
|
||||
`Activate project documentation`
|
||||
--------------------------------
|
||||
|
||||
Pagure offers the option to have a git repository specific for the
|
||||
documentation of the project.
|
||||
|
||||
This repository is then accessible under the ``Docs`` tab in the menu of the
|
||||
project.
|
||||
|
||||
If you prefer to store your documentation elsewhere or maybe even within
|
||||
the sources of the project, you can disable the ``Docs`` tab by un-checking
|
||||
this option.
|
||||
|
||||
|
||||
`Activate pull requests`
|
||||
------------------------
|
||||
|
||||
Pagure offers the option to fork a project, make changes to it and then ask
|
||||
the developer to merge these changes into the project. This is similar to
|
||||
the pull-request mechanism on GitHub or GitLab.
|
||||
|
||||
However, some projects may prefer receiving patches by email on their list
|
||||
or via another hosting plateform or simply do not wish to use the
|
||||
pull-request mechanism at all. Un-checking this option will therefore
|
||||
prevent anyone from opening a pull-request against this project.
|
||||
|
||||
.. note:: disabling pull-requests does *not* disable forking the projects.
|
||||
|
||||
|
||||
`Activate Web-hooks`
|
||||
--------------------
|
||||
|
||||
Pagure offers the option of sending notification about event happening on a
|
||||
project via [web-hooks|https://en.wikipedia.org/wiki/Webhook]. This option
|
||||
is off by default and can be turned on for a pagure instance in its
|
||||
configuration file.
|
||||
|
||||
The URL of the web-hooks can be entered in this field.
|
||||
|
||||
.. note:: See the ``notifications`` documentation to learn more about
|
||||
web-hooks in pagure and how to use them.
|
35
doc/usage/roadmap.rst
Normal file
|
@ -0,0 +1,35 @@
|
|||
Using the roadmap feature
|
||||
=========================
|
||||
|
||||
Pagure allows building the roadmap of the project using the tickets and
|
||||
their tags.
|
||||
|
||||
The principal is as follow:
|
||||
|
||||
* All the ticket with the tag ``roadmap`` will show up on the roadmap page.
|
||||
* For each milestones defined in the settings of the project, the roadmap
|
||||
will group tickets with the corresponding tag.
|
||||
* Tickets with the tag ``roadmap`` that are not associated with any of the
|
||||
milestones defined in the settings are group in an ``unplanned`` section.
|
||||
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
For a project named ``test`` on ``pagure.io``.
|
||||
|
||||
|
||||
|
||||
* First, go to the settings page of the project, create the milestones you
|
||||
like, for example: ``v1.0`` and ``v2.0``.
|
||||
|
||||
* For the tickets you want to be on these milestones, go through each of them
|
||||
and add them the tags: ``roadmap`` in combination with the milestone you want
|
||||
``v1.0`` or ``v2.0``, or none of them if the ticket is on the roadmap but
|
||||
not assigned to any milestones.
|
||||
|
||||
|
||||
* And this is how it will look like
|
||||
|
||||
.. image:: _static/pagure_roadmap2.png
|
||||
:target: _static/pagure_roadmap2.png
|
81
doc/usage/theming.rst
Normal file
|
@ -0,0 +1,81 @@
|
|||
Theme your pagure
|
||||
=================
|
||||
|
||||
Pagure via `flask-multistatic <https://pagure.io/flask-multistatic>`_
|
||||
offers the possibility to override the default theme allowing to customize
|
||||
the style of your instance.
|
||||
|
||||
By default pagure looks for its templates and static files in the folders
|
||||
``pagure/templates`` and ``pagure/static``, but you can ask pagure to look
|
||||
for templates and static files in another folder.
|
||||
|
||||
By specifying the configuration keys ``THEME_TEMPLATE_FOLDER`` and
|
||||
``THEME_STATIC_FOLDER`` in pagure's configuration file, you tell pagure to
|
||||
look for templates and static files first in these folders, then in its
|
||||
usual folders.
|
||||
|
||||
|
||||
.. note: The principal is that pagure will look in the folder specified in
|
||||
the configuration file first and then in its usual folder, so the
|
||||
**file names must be identical**.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
Let's take an example, you wish to replace the pagure logo at the top right
|
||||
of all the pages.
|
||||
|
||||
This logo is part of the ``master.html`` template which all pages inherit
|
||||
from. So what you want to do is replace this ``master.html`` by your own.
|
||||
|
||||
* First, create the folder where your templates and static files will be stored:
|
||||
|
||||
::
|
||||
|
||||
mkdir /var/www/mypaguretheme/templates
|
||||
mkdir /var/www/mypaguretheme/static
|
||||
|
||||
* Place your own logo in the static folder
|
||||
|
||||
::
|
||||
|
||||
cp /path/to/your/my-logo.png /var/www/mypaguretheme/static
|
||||
|
||||
* Place in there the original ``master.html``
|
||||
|
||||
::
|
||||
|
||||
cp /path/to/original/pagure/templates/master.html /var/www/mypaguretheme/templates
|
||||
|
||||
* Edit it and replace the url pointing to the pagure logo (around line 27)
|
||||
|
||||
::
|
||||
|
||||
- <img height=40px src="{{ url_for('static', filename='pagure-logo.png') }}"
|
||||
+ <img height=40px src="{{ url_for('static', filename='my-logo.png') }}"
|
||||
|
||||
* Adjust pagure's configuration file:
|
||||
|
||||
+ THEME_TEMPLATE_FOLDER='/var/www/mypaguretheme/templates'
|
||||
+ THEME_STATIC_FOLDER='/var/www/mypaguretheme/static'
|
||||
|
||||
* Restart pagure
|
||||
|
||||
|
||||
.. note: you could just have replaced the `pagure-logo.png` file with your
|
||||
own logo which would have avoided overriding the template.
|
||||
|
||||
|
||||
In production
|
||||
-------------
|
||||
|
||||
Serving static files via flask is fine for development but in production
|
||||
you will probably want to have apache server them. This will allow caching
|
||||
either on the server side or on the client side.
|
||||
|
||||
You can ask apache to behave in a similar way as does flask-multistatic with
|
||||
flask here, ie: search in one folder and if you don't find the file look
|
||||
in another one.
|
||||
|
||||
`An example apache configuration <https://pagure.io/flask-multistatic/blob/master/f/example.conf>`_
|
||||
is provided as part of the sources of `flask-multistatic <https://pagure.io/flask-multistatic/>`_.
|
77
doc/usage/ticket_templates.rst
Normal file
|
@ -0,0 +1,77 @@
|
|||
Templates for ticket input
|
||||
==========================
|
||||
|
||||
Pagure offers the possibility to add templates for ticket's input. These
|
||||
templates do not enforce anything, users will have the possibility to simply
|
||||
ignore it, or even to not follow it, but it also helps structuring the
|
||||
ticket opened against a project and highlighting the information that are
|
||||
often requested/needed.
|
||||
|
||||
The templates are provided in the git repository containing the meta-data
|
||||
for the tickets.
|
||||
They must be placed under a ``templates`` folder in this git repository,
|
||||
end with ``.md``and as the extension suggests can be formated as markdown.
|
||||
|
||||
If you create a template ``templates/default.md``, it will be shown by
|
||||
default when someone ask to create a new ticket.
|
||||
|
||||
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
For a project named ``test`` on ``pagure.io``.
|
||||
|
||||
* First, clone the ticket git repo [#f1]_ and move into it
|
||||
|
||||
::
|
||||
|
||||
git clone ssh://git@pagure.io/tickets/pagure.git
|
||||
cd test
|
||||
|
||||
* Create the templates folder
|
||||
|
||||
::
|
||||
|
||||
mkdir templates
|
||||
|
||||
* Create a default template
|
||||
|
||||
::
|
||||
|
||||
vim templates/default.md
|
||||
|
||||
And place in this file the following content:
|
||||
|
||||
::
|
||||
|
||||
##### Issue
|
||||
|
||||
##### Steps to reproduce
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
##### Actual results
|
||||
|
||||
##### Expected results
|
||||
|
||||
* Commit and push the changes to the git repo
|
||||
|
||||
::
|
||||
|
||||
git add templates
|
||||
git commit -m "Add a default template for tickets"
|
||||
git push
|
||||
|
||||
|
||||
* And this is how it will look like
|
||||
|
||||
.. image:: _static/pagure_ticket_template.png
|
||||
:target: _static/pagure_ticket_template.png
|
||||
|
||||
|
||||
|
||||
.. [#f1] All the URLs to the different git repositories can be found on the
|
||||
main page of the project, on the right-side menu, under the section
|
||||
``Source GIT URLs``, click on ``more`` to see them.
|
48
doc/usage/upgrade_db.rst
Normal file
|
@ -0,0 +1,48 @@
|
|||
Upgrade a database
|
||||
==================
|
||||
|
||||
|
||||
Database schema migration are handled in two ways:
|
||||
|
||||
* New tables
|
||||
|
||||
For this we simply rely on the ``createdb`` script used when creating the
|
||||
database the first time.
|
||||
|
||||
* Changes to existing tables
|
||||
|
||||
For changes to existing tables, we rely on `Alembic <http://alembic.readthedocs.org/>`_.
|
||||
This allows us to do upgrade and downgrade of schema migration, kind of like
|
||||
one would do commits in a system like git.
|
||||
|
||||
To upgrade the database to the latest version simply run:
|
||||
::
|
||||
|
||||
alembic upgrade head
|
||||
|
||||
This may fail for different reasons:
|
||||
|
||||
* The change was already made in the database
|
||||
|
||||
This can be because the version of the database schema saved is incorrect.
|
||||
It can be debugged using the following commands:
|
||||
|
||||
* Find the current revision: ``alembic current``
|
||||
* See the entire history: ``alembic history``
|
||||
|
||||
Once the revision at which your database should be is found (in the history)
|
||||
you can declare that your database is at this given revision using:
|
||||
``alembic stamp <revision id>``.
|
||||
|
||||
Eventually, if you do not know where your database is or should be, you can
|
||||
do an iterative process stamping the database for every revision, one by one
|
||||
trying everytime to ``alembic upgrade`` until it works.
|
||||
|
||||
* The database used does not support some of the changes
|
||||
|
||||
SQLite is handy for development but does not support all the features of a
|
||||
real database server. Upgrading a SQLite database might therefore not work,
|
||||
depending on the changes done.
|
||||
|
||||
In some cases, if you are using a SQLite database, you will have to destroy
|
||||
it and create a new one.
|
108
doc/usage/using_doc.rst
Normal file
|
@ -0,0 +1,108 @@
|
|||
Using the doc repository of your project
|
||||
========================================
|
||||
|
||||
In this section of the documentation, we are interested in the doc repository.
|
||||
|
||||
The doc repository is a simple git repo, whose content will appear under the
|
||||
`Docs` tab in pagure and on https://docs.pagure.org/<project>/.
|
||||
|
||||
There are a few ways you can put your documentation in this repo:
|
||||
|
||||
* Simple text files
|
||||
|
||||
Pagure will display them as plain text. If one of these is named ``index``
|
||||
it will be presented as the front page.
|
||||
|
||||
* rst or markdown files
|
||||
|
||||
Pagure will convert them to html on the fly and display them as such.
|
||||
The rst files must end with `.rst` and the markdown ones must end with
|
||||
``.mk``, ``.md`` or simply ``.markdown``.
|
||||
|
||||
* html files
|
||||
|
||||
Pagure will simply show them as such.
|
||||
|
||||
|
||||
.. note: By default the `Docs` tab in the project's menu is disabled, you
|
||||
will have to visit the project's settings page and turn it on
|
||||
in the ``Project options`` section.
|
||||
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
Pagure's documentation is kept in pagure's sources, in the `doc` folder there.
|
||||
You can see it at: `https://pagure.io/pagure/blob/master/f/doc
|
||||
<https://pagure.io/pagure/blob/master/f/doc>`_. This doc can be built with
|
||||
`sphinx <http://sphinx-doc.org/>`_ to make it html and prettier.
|
||||
|
||||
The built documentation is available at: `https://docs.pagure.org/pagure/
|
||||
<https://docs.pagure.org/pagure/>`_.
|
||||
|
||||
This is how it is built/updated:
|
||||
|
||||
* Clone pagure's sources::
|
||||
|
||||
git clone https://pagure.io/docs/pagure.git
|
||||
|
||||
* Move into its doc folder::
|
||||
|
||||
cd pagure/doc
|
||||
|
||||
* Build the doc::
|
||||
|
||||
make html
|
||||
|
||||
* Clone pagure's doc repository::
|
||||
|
||||
git clone ssh://git@pagure.io/docs/pagure.git
|
||||
|
||||
* Copy the result of sphinx's build to the doc repo::
|
||||
|
||||
cp -r _build/html/* pagure/
|
||||
|
||||
* Go into the doc repo and update it::
|
||||
|
||||
cd pagure
|
||||
git add .
|
||||
git commit -am "Update documentation"
|
||||
git push
|
||||
|
||||
* Clean the sources::
|
||||
|
||||
cd ..
|
||||
rm -rf pagure # remove the doc repo
|
||||
rm -rf _build # remove the output from the sphinx's build
|
||||
|
||||
|
||||
To make things simpler, the following script (name `update_doc.sh`) can be
|
||||
used:
|
||||
|
||||
::
|
||||
|
||||
#!/bin/bash
|
||||
|
||||
make html
|
||||
|
||||
git clone "ssh://git@pagure.io/docs/$1.git"
|
||||
cp -r _build/html/* $1/
|
||||
(
|
||||
cd $1
|
||||
git add .
|
||||
git commit -av
|
||||
git push
|
||||
)
|
||||
|
||||
rm -rfI _build
|
||||
rm -rfI $1
|
||||
|
||||
It can be used by running `update_doc.sh <project>` from within the folder
|
||||
containing the doc.
|
||||
|
||||
So for pagure it would be something like:
|
||||
|
||||
::
|
||||
|
||||
cd pagure/doc
|
||||
update_doc.sh pagure
|
57
doc/usage/using_webhooks.rst
Normal file
|
@ -0,0 +1,57 @@
|
|||
Using web-hooks
|
||||
===============
|
||||
|
||||
Web-hooks are a notification system that could be compared to a callback.
|
||||
Basically, pagure will make a HTTP POST request to one or more third party
|
||||
server/application with information about what is or just happened.
|
||||
|
||||
To set-up a web-hook, simply go to the settings page of your project and
|
||||
enter the URL to the server/endpoint that will receive the notifications.
|
||||
|
||||
There is, in the settings page, a web-hook key which is used by the
|
||||
server (here pagure) to sign the message sent and which you can use to
|
||||
ensure the notifications received are coming from the right source.
|
||||
|
||||
Each POST request made contains two specific headers:
|
||||
|
||||
::
|
||||
|
||||
X-Pagure-Topic
|
||||
X-Pagure-Signature
|
||||
|
||||
|
||||
``X-Pagure-Topic`` is a global header giving a clue about the type of action
|
||||
that just occured. For example ``issue.edit``.
|
||||
|
||||
|
||||
``X-Pagure-Signature`` contains the signature of the message allowing to
|
||||
check that the message comes from pagure.
|
||||
|
||||
.. warning:: These headers are present for convenience only, they are not
|
||||
signed and therefore should not be trusted. Rely on the payload
|
||||
after checking the signature to make any decision.
|
||||
|
||||
Pagure relies on ``hmac`` to sign the content of its messages. If you want
|
||||
to validate the message, in python, you can do something like the following:
|
||||
|
||||
::
|
||||
|
||||
import hmac
|
||||
import hashlib
|
||||
|
||||
payload = # content you received in the POST request
|
||||
headers = # headers of the POST request
|
||||
project_web_hook_key = # private web-hook key of the project
|
||||
|
||||
hashhex = hmac.new(
|
||||
str(project_web_hook_key), payload, hashlib.sha1).hexdigest()
|
||||
|
||||
if hashhex != headers.get('X-Pagure-Signature'):
|
||||
raise Exception('Message received with an invalid signature')
|
||||
|
||||
|
||||
The notifications sent via web-hooks have the same payload as what is sent
|
||||
via `fedmsg <http://www.fedmsg.com/en/latest/>`_. Therefore, the list of
|
||||
pagure topics as well as example messages can be found in the
|
||||
`fedmsg documentation about pagure
|
||||
<https://fedora-fedmsg.readthedocs.org/en/latest/topics.html#id532>`_
|
248
ev-server/pagure-stream-server.py
Normal file
|
@ -0,0 +1,248 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
"""
|
||||
(c) 2015 - Copyright Red Hat Inc
|
||||
|
||||
Authors:
|
||||
Pierre-Yves Chibon <pingou@pingoured.fr>
|
||||
|
||||
|
||||
Streaming server for pagure's eventsource feature
|
||||
This server takes messages sent to redis and publish them at the specified
|
||||
endpoint
|
||||
|
||||
To test, run this script and in another terminal
|
||||
nc localhost 8080
|
||||
HELLO
|
||||
|
||||
GET /test/issue/26?foo=bar HTTP/1.1
|
||||
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
import os
|
||||
import urlparse
|
||||
|
||||
import trollius
|
||||
import trollius_redis
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
if 'PAGURE_CONFIG' not in os.environ \
|
||||
and os.path.exists('/etc/pagure/pagure.cfg'):
|
||||
print 'Using configuration file `/etc/pagure/pagure.cfg`'
|
||||
os.environ['PAGURE_CONFIG'] = '/etc/pagure/pagure.cfg'
|
||||
|
||||
|
||||
import pagure
|
||||
import pagure.lib
|
||||
from pagure.exceptions import PagureEvException
|
||||
|
||||
SERVER = None
|
||||
|
||||
def get_obj_from_path(path):
|
||||
""" Return the Ticket or Request object based on the path provided.
|
||||
"""
|
||||
username = None
|
||||
try:
|
||||
if path.startswith('/fork'):
|
||||
username, repo, obj, objid = path.split('/')[2:6]
|
||||
else:
|
||||
repo, obj, objid = path.split('/')[1:4]
|
||||
except:
|
||||
raise PagureEvException("Invalid URL: %s" % path)
|
||||
|
||||
repo = pagure.lib.get_project(pagure.SESSION, repo, user=username)
|
||||
|
||||
if repo is None:
|
||||
raise PagureEvException("Project '%s' not found" % repo)
|
||||
|
||||
output = None
|
||||
if obj == 'issue':
|
||||
if not repo.settings.get('issue_tracker', True):
|
||||
raise PagureEvException("No issue tracker found for this project")
|
||||
|
||||
output = pagure.lib.search_issues(
|
||||
pagure.SESSION, repo, issueid=objid)
|
||||
|
||||
if output is None or output.project != repo:
|
||||
raise PagureEvException("Issue '%s' not found" % objid)
|
||||
|
||||
if output.private:
|
||||
# TODO: find a way to do auth
|
||||
raise PagureEvException(
|
||||
"This issue is private and you are not allowed to view it")
|
||||
elif obj == 'pull-request':
|
||||
if not repo.settings.get('pull_requests', True):
|
||||
raise PagureEvException(
|
||||
"No pull-request tracker found for this project")
|
||||
|
||||
output = pagure.lib.search_pull_requests(
|
||||
pagure.SESSION, project_id=repo.id, requestid=objid)
|
||||
|
||||
if output is None or output.project != repo:
|
||||
raise PagureEvException("Pull-Request '%s' not found" % objid)
|
||||
|
||||
else:
|
||||
raise PagureEvException("Invalid object provided: '%s'" % obj)
|
||||
|
||||
return output
|
||||
|
||||
|
||||
@trollius.coroutine
|
||||
def handle_client(client_reader, client_writer):
|
||||
data = None
|
||||
while True:
|
||||
# give client a chance to respond, timeout after 10 seconds
|
||||
line = yield trollius.From(trollius.wait_for(
|
||||
client_reader.readline(),
|
||||
timeout=10.0))
|
||||
if not line.decode().strip():
|
||||
break
|
||||
line = line.decode().rstrip()
|
||||
if data is None:
|
||||
data = line
|
||||
|
||||
if data is None:
|
||||
log.warning("Expected ticket uid, received None")
|
||||
return
|
||||
|
||||
data = data.decode().rstrip().split()
|
||||
log.info("Received %s", data)
|
||||
if not data:
|
||||
log.warning("No URL provided: %s" % data)
|
||||
return
|
||||
|
||||
if not '/' in data[1]:
|
||||
log.warning("Invalid URL provided: %s" % data[1])
|
||||
return
|
||||
|
||||
url = urlparse.urlsplit(data[1])
|
||||
|
||||
try:
|
||||
obj = get_obj_from_path(url.path)
|
||||
except PagureEvException as err:
|
||||
log.warning(err.message)
|
||||
return
|
||||
|
||||
origin = pagure.APP.config.get('APP_URL')
|
||||
if origin.endswith('/'):
|
||||
origin = origin[:-1]
|
||||
|
||||
client_writer.write((
|
||||
"HTTP/1.0 200 OK\n"
|
||||
"Content-Type: text/event-stream\n"
|
||||
"Cache: nocache\n"
|
||||
"Connection: keep-alive\n"
|
||||
"Access-Control-Allow-Origin: %s\n\n" % origin
|
||||
).encode())
|
||||
|
||||
connection = yield trollius.From(trollius_redis.Connection.create(
|
||||
host=pagure.APP.config['REDIS_HOST'],
|
||||
port=pagure.APP.config['REDIS_PORT'],
|
||||
db=pagure.APP.config['REDIS_DB']))
|
||||
|
||||
try:
|
||||
|
||||
# Create subscriber.
|
||||
subscriber = yield trollius.From(connection.start_subscribe())
|
||||
|
||||
# Subscribe to channel.
|
||||
yield trollius.From(subscriber.subscribe(['pagure.%s' % obj.uid]))
|
||||
|
||||
# Inside a while loop, wait for incoming events.
|
||||
while True:
|
||||
reply = yield trollius.From(subscriber.next_published())
|
||||
#print(u'Received: ', repr(reply.value), u'on channel', reply.channel)
|
||||
log.info(reply)
|
||||
log.info("Sending %s", reply.value)
|
||||
client_writer.write(('data: %s\n\n' % reply.value).encode())
|
||||
yield trollius.From(client_writer.drain())
|
||||
|
||||
except trollius.ConnectionResetError as err:
|
||||
log.exception("ERROR: ConnectionResetError in handle_client")
|
||||
except Exception as err:
|
||||
log.exception("ERROR: Exception in handle_client")
|
||||
finally:
|
||||
# Wathever happens, close the connection.
|
||||
connection.close()
|
||||
client_writer.close()
|
||||
|
||||
|
||||
@trollius.coroutine
|
||||
def stats(client_reader, client_writer):
|
||||
|
||||
try:
|
||||
log.info('Clients: %s', SERVER.active_count)
|
||||
client_writer.write((
|
||||
"HTTP/1.0 200 OK\n"
|
||||
"Cache: nocache\n\n"
|
||||
).encode())
|
||||
client_writer.write(('data: %s\n\n' % SERVER.active_count).encode())
|
||||
yield trollius.From(client_writer.drain())
|
||||
|
||||
except trollius.ConnectionResetError as err:
|
||||
log.info(err)
|
||||
pass
|
||||
finally:
|
||||
client_writer.close()
|
||||
return
|
||||
|
||||
|
||||
def main():
|
||||
global SERVER
|
||||
|
||||
try:
|
||||
loop = trollius.get_event_loop()
|
||||
coro = trollius.start_server(
|
||||
handle_client,
|
||||
host=None,
|
||||
port=pagure.APP.config['EVENTSOURCE_PORT'],
|
||||
loop=loop)
|
||||
SERVER = loop.run_until_complete(coro)
|
||||
log.info('Serving server at {}'.format(SERVER.sockets[0].getsockname()))
|
||||
if pagure.APP.config.get('EV_STATS_PORT'):
|
||||
stats_coro = trollius.start_server(
|
||||
stats,
|
||||
host=None,
|
||||
port=pagure.APP.config.get('EV_STATS_PORT'),
|
||||
loop=loop)
|
||||
stats_server = loop.run_until_complete(stats_coro)
|
||||
log.info('Serving stats at {}'.format(
|
||||
stats_server.sockets[0].getsockname()))
|
||||
loop.run_forever()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
except trollius.ConnectionResetError as err:
|
||||
log.exception("ERROR: ConnectionResetError in main")
|
||||
except Exception as err:
|
||||
log.exception("ERROR: Exception in main")
|
||||
finally:
|
||||
# Close the server
|
||||
SERVER.close()
|
||||
if pagure.APP.config.get('EV_STATS_PORT'):
|
||||
stats_server.close()
|
||||
log.info("End Connection")
|
||||
loop.run_until_complete(SERVER.wait_closed())
|
||||
loop.close()
|
||||
log.info("End")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
log = logging.getLogger("")
|
||||
formatter = logging.Formatter(
|
||||
"%(asctime)s %(levelname)s [%(module)s:%(lineno)d] %(message)s")
|
||||
|
||||
# setup console logging
|
||||
log.setLevel(logging.DEBUG)
|
||||
ch = logging.StreamHandler()
|
||||
ch.setLevel(logging.DEBUG)
|
||||
|
||||
aslog = logging.getLogger("asyncio")
|
||||
aslog.setLevel(logging.DEBUG)
|
||||
|
||||
ch.setFormatter(formatter)
|
||||
log.addHandler(ch)
|
||||
main()
|
14
ev-server/pagure_ev.service
Normal file
|
@ -0,0 +1,14 @@
|
|||
[Unit]
|
||||
Description=Pagure EventSource server (Allowing live refresh of the pages supporting it)
|
||||
After=redis.target
|
||||
Documentation=https://pagure.io/pagure
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/libexec/pagure-ev/pagure-stream-server.py
|
||||
Type=simple
|
||||
User=git
|
||||
Group=git
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
60
files/alembic.ini
Normal file
|
@ -0,0 +1,60 @@
|
|||
# A generic, single database configuration.
|
||||
|
||||
[alembic]
|
||||
# path to migration scripts
|
||||
script_location = /usr/share/pagure/alembic
|
||||
|
||||
# template used to generate migration files
|
||||
# file_template = %%(rev)s_%%(slug)s
|
||||
|
||||
# max length of characters to apply to the
|
||||
# "slug" field
|
||||
#truncate_slug_length = 40
|
||||
|
||||
# set to 'true' to run the environment during
|
||||
# the 'revision' command, regardless of autogenerate
|
||||
# revision_environment = false
|
||||
|
||||
# set to 'true' to allow .pyc and .pyo files without
|
||||
# a source .py file to be detected as revisions in the
|
||||
# versions/ directory
|
||||
# sourceless = false
|
||||
|
||||
#sqlalchemy.url = driver://user:pass@localhost/dbname
|
||||
sqlalchemy.url = sqlite:////var/tmp/pagure_dev.sqlite
|
||||
|
||||
|
||||
# Logging configuration
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARN
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARN
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
55
files/api_key_expire_mail.py
Normal file
|
@ -0,0 +1,55 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import os
|
||||
import argparse
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
|
||||
|
||||
if 'PAGURE_CONFIG' not in os.environ \
|
||||
and os.path.exists('/etc/pagure/pagure.cfg'):
|
||||
print 'Using configuration file `/etc/pagure/pagure.cfg`'
|
||||
os.environ['PAGURE_CONFIG'] = '/etc/pagure/pagure.cfg'
|
||||
|
||||
import pagure
|
||||
from pagure import SESSION
|
||||
from pagure.lib import model
|
||||
|
||||
|
||||
def main(debug=False):
|
||||
''' The function that actually sends the email
|
||||
in case the expiration date is near'''
|
||||
|
||||
current_time = datetime.utcnow()
|
||||
day_diff_for_mail = [5, 3, 1]
|
||||
email_dates = [email_day.date() for email_day in \
|
||||
[current_time + timedelta(days=i) for i in day_diff_for_mail]]
|
||||
|
||||
tokens = SESSION.query(model.Token).all()
|
||||
|
||||
for token in tokens:
|
||||
if token.expiration.date() in email_dates:
|
||||
user = token.user
|
||||
user_email = user.default_email
|
||||
project = token.project
|
||||
days_left = token.expiration.day - datetime.utcnow().day
|
||||
subject = 'Pagure API key expiration date is near!'
|
||||
text = '''Hi %s, \nYour Pagure API key for the project %s will expire
|
||||
in %s day(s). Please get a new key for non-interrupted service. \n
|
||||
Thanks, \nYour Pagure Admin. ''' % (user.fullname, project.name, days_left)
|
||||
msg = pagure.lib.notify.send_email(text, subject, user_email)
|
||||
if debug:
|
||||
print 'Sent mail to %s' % user.fullname
|
||||
if debug:
|
||||
print 'Done'
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Script to send email before the api token expires')
|
||||
parser.add_argument(
|
||||
'--debug', dest='debug', action='store_true', default=False,
|
||||
help='Print the debugging output')
|
||||
args = parser.parse_args()
|
||||
main(debug=args.debug)
|
23
files/doc_pagure.wsgi
Normal file
|
@ -0,0 +1,23 @@
|
|||
#-*- coding: utf-8 -*-
|
||||
|
||||
# The three lines below are required to run on EL6 as EL6 has
|
||||
# two possible version of python-sqlalchemy and python-jinja2
|
||||
# These lines make sure the application uses the correct version.
|
||||
import __main__
|
||||
__main__.__requires__ = ['SQLAlchemy >= 0.8', 'jinja2 >= 2.4']
|
||||
import pkg_resources
|
||||
|
||||
# Set the environment variable pointing to the configuration file
|
||||
import os
|
||||
os.environ['PAGURE_CONFIG'] = '/etc/pagure/pagure.cfg'
|
||||
|
||||
|
||||
# The following is only needed if you did not install pagure
|
||||
# as a python module (for example if you run it from a git clone).
|
||||
#import sys
|
||||
#sys.path.insert(0, '/path/to/pagure/')
|
||||
|
||||
|
||||
# The most import line to make the wsgi working
|
||||
from pagure.docs_server import APP as application
|
||||
#application.debug = True
|
32
files/emoji_clean_json.py
Normal file
|
@ -0,0 +1,32 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
data = None
|
||||
with open('emoji_strategy.json') as stream:
|
||||
data = json.load(stream)
|
||||
|
||||
if not data:
|
||||
print 'Could not load the data from the JSON file'
|
||||
sys.exit(1)
|
||||
|
||||
# Retrieve the items we keep in the JSON
|
||||
tokeep = {}
|
||||
for key in data:
|
||||
if '-' in data[key]['unicode'] and data[key]['unicode'].startswith('1F'):
|
||||
continue
|
||||
tokeep[key] = data[key]
|
||||
|
||||
# Check if we have the keys of all images we kept
|
||||
|
||||
unicodes = [tokeep[key]['unicode'] for key in tokeep]
|
||||
images = [item.replace('.png', '') for item in os.listdir('png')]
|
||||
|
||||
print set(unicodes).symmetric_difference(set(images))
|
||||
|
||||
|
||||
with open('emoji_strategy2.json', 'w') as stream:
|
||||
json.dump(tokeep, stream)
|
||||
|
233
files/gitolite.rc
Executable file
|
@ -0,0 +1,233 @@
|
|||
# paths and configuration variables for gitolite
|
||||
|
||||
# please read comments before editing
|
||||
|
||||
# this file is meant to be pulled into a perl program using "do" or "require".
|
||||
|
||||
# You do NOT need to know perl to edit the paths; it should be fairly
|
||||
# self-explanatory and easy to maintain perl syntax :-)
|
||||
|
||||
# --------------------------------------
|
||||
# Do not uncomment these values unless you know what you're doing
|
||||
# $GL_PACKAGE_CONF = "";
|
||||
# $GL_PACKAGE_HOOKS = "";
|
||||
|
||||
# --------------------------------------
|
||||
|
||||
# --------------------------------------
|
||||
|
||||
# this is where the repos go. If you provide a relative path (not starting
|
||||
# with "/"), it's relative to your $HOME. You may want to put in something
|
||||
# like "/bigdisk" or whatever if your $HOME is too small for the repos, for
|
||||
# example
|
||||
|
||||
$REPO_BASE="/srv/git/repositories/";
|
||||
|
||||
# the default umask for repositories is 0077; change this if you run stuff
|
||||
# like gitweb and find it can't read the repos. Please note the syntax; the
|
||||
# leading 0 is required
|
||||
|
||||
$REPO_UMASK = 0002;
|
||||
# $REPO_UMASK = 0027; # gets you 'rwxr-x---'
|
||||
# $REPO_UMASK = 0022; # gets you 'rwxr-xr-x'
|
||||
|
||||
# part of the setup of gitweb is a variable called $projects_list (please see
|
||||
# gitweb documentation for more on this). Set this to the same value:
|
||||
|
||||
$PROJECTS_LIST = $ENV{HOME} . "/projects.list";
|
||||
|
||||
# --------------------------------------
|
||||
|
||||
# I see no reason anyone may want to change the gitolite admin directory, but
|
||||
# feel free to do so. However, please note that it *must* be an *absolute*
|
||||
# path (i.e., starting with a "/" character)
|
||||
|
||||
# gitolite admin directory, files, etc
|
||||
|
||||
$GL_ADMINDIR="/etc/gitolite";
|
||||
|
||||
# --------------------------------------
|
||||
|
||||
# templates for location of the log files and format of their names
|
||||
|
||||
# I prefer this template (note the %y and %m placeholders)
|
||||
# it produces files like `~/.gitolite/logs/gitolite-2009-09.log`
|
||||
|
||||
$GL_LOGT="/var/log/gitolite/gitolite-%y-%m.log";
|
||||
|
||||
# other choices are below, or you can make your own -- but PLEASE MAKE SURE
|
||||
# the directory exists and is writable; gitolite won't do that for you (unless
|
||||
# it is the default, which is "$GL_ADMINDIR/logs")
|
||||
|
||||
# $GL_LOGT="$GL_ADMINDIR/logs/gitolite-%y-%m-%d.log";
|
||||
# $GL_LOGT="$GL_ADMINDIR/logs/gitolite-%y.log";
|
||||
|
||||
# --------------------------------------
|
||||
|
||||
# Please DO NOT change these three paths
|
||||
|
||||
$GL_CONF="$GL_ADMINDIR/conf/gitolite.conf";
|
||||
$GL_KEYDIR="$GL_ADMINDIR/keydir";
|
||||
$GL_CONF_COMPILED="$GL_ADMINDIR/conf/gitolite.conf-compiled.pm";
|
||||
|
||||
# --------------------------------------
|
||||
|
||||
# if git on your server is on a standard path (that is
|
||||
# ssh git@server git --version
|
||||
# works), leave this setting as is. Otherwise, choose one of the
|
||||
# alternatives, or write your own
|
||||
|
||||
$GIT_PATH="";
|
||||
# $GIT_PATH="/opt/bin/";
|
||||
|
||||
# --------------------------------------
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# BIG CONFIG SETTINGS
|
||||
|
||||
# Please read doc/big-config.mkd for details
|
||||
|
||||
$GL_BIG_CONFIG = 1;
|
||||
$GL_NO_DAEMON_NO_GITWEB = 1;
|
||||
$GL_NO_CREATE_REPOS = 1;
|
||||
$GL_NO_SETUP_AUTHKEYS = 1;
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# SECURITY SENSITIVE SETTINGS
|
||||
#
|
||||
# Settings below this point may have security implications. That
|
||||
# usually means that I have not thought hard enough about all the
|
||||
# possible ways to crack security if these settings are enabled.
|
||||
|
||||
# Please see details on each setting for specifics, if any.
|
||||
# ----------------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
# --------------------------------------
|
||||
# ALLOW REPO ADMIN TO SET GITCONFIG KEYS
|
||||
#
|
||||
# Gitolite allows you to set git repo options using the "config" keyword; see
|
||||
# conf/example.conf for details and syntax.
|
||||
#
|
||||
# However, if you are in an installation where the repo admin does not (and
|
||||
# should not) have shell access to the server, then allowing him to set
|
||||
# arbitrary repo config options *may* be a security risk -- some config
|
||||
# settings may allow executing arbitrary commands.
|
||||
#
|
||||
# You have 3 choices. By default $GL_GITCONFIG_KEYS is left empty, which
|
||||
# completely disables this feature (meaning you cannot set git configs from
|
||||
# the repo config).
|
||||
|
||||
$GL_GITCONFIG_KEYS = "";
|
||||
|
||||
# The second choice is to give it a space separated list of settings you
|
||||
# consider safe. (These are actually treated as a set of regular expression
|
||||
# patterns, and any one of them must match). For example:
|
||||
# $GL_GITCONFIG_KEYS = "core\.logAllRefUpdates core\..*compression";
|
||||
# allows repo admins to set one of those 3 config keys (yes, that second
|
||||
# pattern matches two settings from "man git-config", if you look)
|
||||
#
|
||||
# The third choice (which you may have guessed already if you're familiar with
|
||||
# regular expressions) is to allow anything and everything:
|
||||
# $GL_GITCONFIG_KEYS = ".*";
|
||||
|
||||
# --------------------------------------
|
||||
# EXTERNAL COMMAND HELPER -- HTPASSWD
|
||||
|
||||
# security note: runs an external command (htpasswd) with specific arguments,
|
||||
# including a user-chosen "password".
|
||||
|
||||
# if you want to enable the "htpasswd" command, give this the absolute path to
|
||||
# whatever file apache (etc) expect to find the passwords in.
|
||||
|
||||
$HTPASSWD_FILE = "";
|
||||
|
||||
# Look in doc/3 ("easier to link gitweb authorisation with gitolite" section)
|
||||
# for more details on using this feature.
|
||||
|
||||
# --------------------------------------
|
||||
# EXTERNAL COMMAND HELPER -- RSYNC
|
||||
|
||||
# security note: runs an external command (rsync) with specific arguments, all
|
||||
# presumably filled in correctly by the client-side rsync.
|
||||
|
||||
# base path of all the files that are accessible via rsync. Must be an
|
||||
# absolute path. Leave it undefined or set to the empty string to disable the
|
||||
# rsync helper.
|
||||
|
||||
$RSYNC_BASE = "";
|
||||
|
||||
# $RSYNC_BASE = "/home/git/up-down";
|
||||
# $RSYNC_BASE = "/tmp/up-down";
|
||||
|
||||
# --------------------------------------
|
||||
# EXTERNAL COMMAND HELPER -- SVNSERVE
|
||||
|
||||
# security note: runs an external command (svnserve) with specific arguments,
|
||||
# as specified below. %u is substituted with the username.
|
||||
|
||||
# This setting allows launching svnserve when requested by the ssh client.
|
||||
# This allows using the same SSH setup (hostname/username/public key) for both
|
||||
# SVN and git access. Leave it undefined or set to the empty string to disable
|
||||
# svnserve access.
|
||||
|
||||
$SVNSERVE = "";
|
||||
# $SVNSERVE = "/usr/bin/svnserve -r /var/svn/ -t --tunnel-user=%u";
|
||||
|
||||
# --------------------------------------
|
||||
# ALLOW REPO CONFIG TO USE WILDCARDS
|
||||
|
||||
# security note: this used to in a separate "wildrepos" branch. You can
|
||||
# create repositories based on wild cards, give "ownership" to the specific
|
||||
# user who created it, allow him/her to hand out R and RW permissions to other
|
||||
# users to collaborate, etc. This is powerful stuff, and I've made it as
|
||||
# secure as I can, but it hasn't had the kind of rigorous line-by-line
|
||||
# analysis that the old "master" branch had.
|
||||
|
||||
# This has now been rolled into master, with all the functionality gated by
|
||||
# this variable. Set this to 1 if you want to enable the wildrepos features.
|
||||
# Please see doc/4-wildcard-repositories.mkd for details.
|
||||
|
||||
$GL_WILDREPOS = 0;
|
||||
|
||||
# --------------------------------------
|
||||
# DEFAULT WILDCARD PERMISSIONS
|
||||
|
||||
# If set, this value will be used as the default user-level permission rule of
|
||||
# new wildcard repositories. The user can change this value with the setperms command
|
||||
# as desired after repository creation; it is only a default. Note that @all can be
|
||||
# used here but is special; no other groups can be used in user-level permissions.
|
||||
|
||||
# $GL_WILDREPOS_DEFPERMS = 'R = @all';
|
||||
|
||||
# --------------------------------------
|
||||
# HOOK CHAINING
|
||||
|
||||
# by default, the update hook in every repo chains to "update.secondary".
|
||||
# Similarly, the post-update hook in the admin repo chains to
|
||||
# "post-update.secondary". If you're fine with the defaults, there's no need
|
||||
# to do anything here. However, if you want to use different names or paths,
|
||||
# change these variables
|
||||
|
||||
# $UPDATE_CHAINS_TO = "hooks/update.secondary";
|
||||
# $ADMIN_POST_UPDATE_CHAINS_TO = "hooks/post-update.secondary";
|
||||
|
||||
# --------------------------------------
|
||||
# ADMIN DEFINED COMMANDS
|
||||
|
||||
# WARNING: Use this feature only if (a) you really really know what you're
|
||||
# doing or (b) you really don't care too much about security. Please read
|
||||
# doc/admin-defined-commands.mkd for details.
|
||||
|
||||
# $GL_ADC_PATH = "";
|
||||
|
||||
# --------------------------------------
|
||||
# per perl rules, this should be the last line in such a file:
|
||||
1;
|
||||
|
||||
# Local variables:
|
||||
# mode: perl
|
||||
# End:
|
||||
# vim: set syn=perl:
|
195
files/gitolite3.rc
Normal file
|
@ -0,0 +1,195 @@
|
|||
# configuration variables for gitolite
|
||||
|
||||
# This file is in perl syntax. But you do NOT need to know perl to edit it --
|
||||
# just mind the commas, use single quotes unless you know what you're doing,
|
||||
# and make sure the brackets and braces stay matched up!
|
||||
|
||||
# (Tip: perl allows a comma after the last item in a list also!)
|
||||
|
||||
# HELP for commands can be had by running the command with "-h".
|
||||
|
||||
# HELP for all the other FEATURES can be found in the documentation (look for
|
||||
# "list of non-core programs shipped with gitolite" in the master index) or
|
||||
# directly in the corresponding source file.
|
||||
|
||||
%RC = (
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
# default umask gives you perms of '0700'; see the rc file docs for
|
||||
# how/why you might change this
|
||||
UMASK => 0077,
|
||||
|
||||
# look for "git-config" in the documentation
|
||||
GIT_CONFIG_KEYS => '',
|
||||
|
||||
# comment out if you don't need all the extra detail in the logfile
|
||||
LOG_EXTRA => 1,
|
||||
# syslog options
|
||||
# 1. leave this section as is for normal gitolite logging
|
||||
# 2. uncomment this line to log only to syslog:
|
||||
# LOG_DEST => 'syslog',
|
||||
# 3. uncomment this line to log to syslog and the normal gitolite log:
|
||||
# LOG_DEST => 'syslog,normal',
|
||||
|
||||
# roles. add more roles (like MANAGER, TESTER, ...) here.
|
||||
# WARNING: if you make changes to this hash, you MUST run 'gitolite
|
||||
# compile' afterward, and possibly also 'gitolite trigger POST_COMPILE'
|
||||
ROLES => {
|
||||
READERS => 1,
|
||||
WRITERS => 1,
|
||||
},
|
||||
|
||||
# enable caching (currently only Redis). PLEASE RTFM BEFORE USING!!!
|
||||
# CACHE => 'Redis',
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
# rc variables used by various features
|
||||
|
||||
# the 'info' command prints this as additional info, if it is set
|
||||
# SITE_INFO => 'Please see http://blahblah/gitolite for more help',
|
||||
|
||||
# the CpuTime feature uses these
|
||||
# display user, system, and elapsed times to user after each git operation
|
||||
# DISPLAY_CPU_TIME => 1,
|
||||
# display a warning if total CPU times (u, s, cu, cs) crosses this limit
|
||||
# CPU_TIME_WARN_LIMIT => 0.1,
|
||||
|
||||
# the Mirroring feature needs this
|
||||
# HOSTNAME => "foo",
|
||||
|
||||
# TTL for redis cache; PLEASE SEE DOCUMENTATION BEFORE UNCOMMENTING!
|
||||
# CACHE_TTL => 600,
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
# suggested locations for site-local gitolite code (see cust.html)
|
||||
|
||||
# this one is managed directly on the server
|
||||
# LOCAL_CODE => "$ENV{HOME}/local",
|
||||
|
||||
# or you can use this, which lets you put everything in a subdirectory
|
||||
# called "local" in your gitolite-admin repo. For a SECURITY WARNING
|
||||
# on this, see http://gitolite.com/gitolite/non-core.html#pushcode
|
||||
# LOCAL_CODE => "$rc{GL_ADMIN_BASE}/local",
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
# List of commands and features to enable
|
||||
|
||||
ENABLE => [
|
||||
|
||||
# COMMANDS
|
||||
|
||||
# These are the commands enabled by default
|
||||
'help',
|
||||
'desc',
|
||||
'info',
|
||||
'perms',
|
||||
'writable',
|
||||
|
||||
# Uncomment or add new commands here.
|
||||
# 'create',
|
||||
# 'fork',
|
||||
# 'mirror',
|
||||
# 'readme',
|
||||
# 'sskm',
|
||||
# 'D',
|
||||
|
||||
# These FEATURES are enabled by default.
|
||||
|
||||
# essential (unless you're using smart-http mode)
|
||||
'ssh-authkeys',
|
||||
|
||||
# creates git-config enties from gitolite.conf file entries like 'config foo.bar = baz'
|
||||
'git-config',
|
||||
|
||||
# creates git-daemon-export-ok files; if you don't use git-daemon, comment this out
|
||||
'daemon',
|
||||
|
||||
# creates projects.list file; if you don't use gitweb, comment this out
|
||||
#'gitweb',
|
||||
|
||||
# These FEATURES are disabled by default; uncomment to enable. If you
|
||||
# need to add new ones, ask on the mailing list :-)
|
||||
|
||||
# user-visible behaviour
|
||||
|
||||
# prevent wild repos auto-create on fetch/clone
|
||||
# 'no-create-on-read',
|
||||
# no auto-create at all (don't forget to enable the 'create' command!)
|
||||
# 'no-auto-create',
|
||||
|
||||
# access a repo by another (possibly legacy) name
|
||||
# 'Alias',
|
||||
|
||||
# give some users direct shell access. See documentation in
|
||||
# sts.html for details on the following two choices.
|
||||
# "Shell $ENV{HOME}/.gitolite.shell-users",
|
||||
# 'Shell alice bob',
|
||||
|
||||
# set default roles from lines like 'option default.roles-1 = ...', etc.
|
||||
# 'set-default-roles',
|
||||
|
||||
# show more detailed messages on deny
|
||||
# 'expand-deny-messages',
|
||||
|
||||
# show a message of the day
|
||||
# 'Motd',
|
||||
|
||||
# system admin stuff
|
||||
|
||||
# enable mirroring (don't forget to set the HOSTNAME too!)
|
||||
# 'Mirroring',
|
||||
|
||||
# allow people to submit pub files with more than one key in them
|
||||
# 'ssh-authkeys-split',
|
||||
|
||||
# selective read control hack
|
||||
# 'partial-copy',
|
||||
|
||||
# manage local, gitolite-controlled, copies of read-only upstream repos
|
||||
# 'upstream',
|
||||
|
||||
# updates 'description' file instead of 'gitweb.description' config item
|
||||
# 'cgit',
|
||||
|
||||
# allow repo-specific hooks to be added
|
||||
# 'repo-specific-hooks',
|
||||
|
||||
# performance, logging, monitoring...
|
||||
|
||||
# be nice
|
||||
# 'renice 10',
|
||||
|
||||
# log CPU times (user, system, cumulative user, cumulative system)
|
||||
# 'CpuTime',
|
||||
|
||||
# syntactic_sugar for gitolite.conf and included files
|
||||
|
||||
# allow backslash-escaped continuation lines in gitolite.conf
|
||||
# 'continuation-lines',
|
||||
|
||||
# create implicit user groups from directory names in keydir/
|
||||
# 'keysubdirs-as-groups',
|
||||
|
||||
# allow simple line-oriented macros
|
||||
# 'macros',
|
||||
|
||||
# Kindergarten mode
|
||||
|
||||
# disallow various things that sensible people shouldn't be doing anyway
|
||||
# 'Kindergarten',
|
||||
],
|
||||
|
||||
);
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# per perl rules, this should be the last line in such a file:
|
||||
1;
|
||||
|
||||
# Local variables:
|
||||
# mode: perl
|
||||
# End:
|
||||
# vim: set syn=perl:
|
118
files/load_from_disk.py
Normal file
|
@ -0,0 +1,118 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import argparse
|
||||
import requests
|
||||
import os
|
||||
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
|
||||
if 'PAGURE_CONFIG' not in os.environ \
|
||||
and os.path.exists('/etc/pagure/pagure.cfg'):
|
||||
print 'Using configuration file `/etc/pagure/pagure.cfg`'
|
||||
os.environ['PAGURE_CONFIG'] = '/etc/pagure/pagure.cfg'
|
||||
|
||||
import pagure
|
||||
|
||||
|
||||
def get_poc_of_pkgs(debug=False):
|
||||
""" Retrieve a dictionary giving the point of contact of each package
|
||||
in pkgdb.
|
||||
"""
|
||||
if debug:
|
||||
print 'Querying pkgdb'
|
||||
PKGDB_URL = 'https://admin.fedoraproject.org/pkgdb/api/'
|
||||
req = requests.get(PKGDB_URL + 'bugzilla').text
|
||||
if debug:
|
||||
print 'Pkgdb data retrieved, getting POC'
|
||||
pkgs = {}
|
||||
for line in req.split('\n'):
|
||||
line = line.strip()
|
||||
if not line or line.startswith('#'):
|
||||
continue
|
||||
line = line.split('|')
|
||||
if len(line) < 4:
|
||||
continue
|
||||
pkgs[line[1]] = line[3]
|
||||
|
||||
return pkgs
|
||||
|
||||
|
||||
def main(folder, debug=False):
|
||||
"""
|
||||
Logic:
|
||||
- Query the list of maintainer/PoC from pkgdb
|
||||
- Browse the directory
|
||||
- For each git in the directory, create the project with the correct POC
|
||||
"""
|
||||
pocs = get_poc_of_pkgs(debug=debug)
|
||||
|
||||
if debug:
|
||||
print 'Adding the user to the DB'
|
||||
for user in sorted(set(pocs.values())):
|
||||
if debug:
|
||||
print user
|
||||
try:
|
||||
pagure.lib.set_up_user(
|
||||
session=pagure.SESSION,
|
||||
username=user,
|
||||
fullname=user,
|
||||
default_email='%s@fedoraproject.org' % user,
|
||||
keydir=pagure.APP.config.get('GITOLITE_KEYDIR', None),
|
||||
)
|
||||
pagure.SESSION.commit()
|
||||
except SQLAlchemyError as err:
|
||||
pagure.SESSION.rollback()
|
||||
print 'ERROR with user %s' % user
|
||||
print err
|
||||
|
||||
for project in sorted(os.listdir(folder)):
|
||||
if debug:
|
||||
print project
|
||||
|
||||
if not project.endswith('.git'):
|
||||
if debug:
|
||||
print ' -skip: not a git repository'
|
||||
continue
|
||||
|
||||
if project.split('.git')[0] not in pocs:
|
||||
if debug:
|
||||
print ' -skip: no pocs'
|
||||
continue
|
||||
|
||||
try:
|
||||
name = project.split('.git')[0]
|
||||
pagure.lib.new_project(
|
||||
session=pagure.SESSION,
|
||||
user=pocs[name],
|
||||
name=name,
|
||||
blacklist=pagure.APP.config['BLACKLISTED_PROJECTS'],
|
||||
gitfolder=pagure.APP.config['GIT_FOLDER'],
|
||||
docfolder=pagure.APP.config['DOCS_FOLDER'],
|
||||
ticketfolder=pagure.APP.config['TICKETS_FOLDER'],
|
||||
requestfolder=pagure.APP.config['REQUESTS_FOLDER'],
|
||||
)
|
||||
pagure.SESSION.commit()
|
||||
except pagure.exceptions.PagureException as err:
|
||||
print 'ERROR with project %s' % project
|
||||
print err
|
||||
except SQLAlchemyError as err: # pragma: no cover
|
||||
pagure.SESSION.rollback()
|
||||
print 'ERROR (DB) with project %s' % project
|
||||
print err
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Script creating projects on pagure based on the git '
|
||||
'repos present in the specified folder and the pkgdb information.'
|
||||
)
|
||||
parser.add_argument(
|
||||
'folder',
|
||||
help='Folder containing all the git repos of the projects to create')
|
||||
parser.add_argument(
|
||||
'--debug', dest='debug', action='store_true', default=False,
|
||||
help='Print the debugging output')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
main(args.folder, debug=args.debug)
|
214
files/pagure.cfg.sample
Normal file
|
@ -0,0 +1,214 @@
|
|||
import os
|
||||
from datetime import timedelta
|
||||
|
||||
### 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.
|
||||
# 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)
|
||||
|
||||
### Secret key for the Flask application
|
||||
SECRET_KEY='<The web application secret key>'
|
||||
|
||||
### url to the database server:
|
||||
#DB_URL=mysql://user:pass@host/db_name
|
||||
#DB_URL=postgres://user:pass@host/db_name
|
||||
DB_URL = 'sqlite:////var/tmp/pagure_dev.sqlite'
|
||||
|
||||
### The FAS group in which the admin of pagure are
|
||||
ADMIN_GROUP = ['sysadmin-main']
|
||||
|
||||
### Hard-coded list of global admins
|
||||
PAGURE_ADMIN_USERS = []
|
||||
|
||||
### The email address to which the flask.log will send the errors (tracebacks)
|
||||
EMAIL_ERROR = 'pingou@pingoured.fr'
|
||||
|
||||
### SMTP settings
|
||||
SMTP_SERVER = 'localhost'
|
||||
SMTP_PORT = 25
|
||||
SMTP_SSL = False
|
||||
|
||||
#Specify both for enabling SMTP with auth
|
||||
SMTP_USERNAME = None
|
||||
SMTP_PASSWORD = None
|
||||
|
||||
### Information used to sent notifications
|
||||
FROM_EMAIL = 'pagure@pagure.io'
|
||||
DOMAIN_EMAIL_NOTIFICATIONS = 'pagure.io'
|
||||
SALT_EMAIL = '<secret key to be changed>'
|
||||
|
||||
### The URL at which the project is available.
|
||||
APP_URL = 'https://pagure.io/'
|
||||
### The URL at which the documentation of projects will be available
|
||||
## This should be in a different domain to avoid XSS issues since we want
|
||||
## to allow raw html to be displayed (different domain, ie not a sub-domain).
|
||||
DOC_APP_URL = 'https://docs.pagure.org'
|
||||
|
||||
### The URL to use to clone git repositories.
|
||||
GIT_URL_SSH = 'ssh://git@pagure.io/'
|
||||
GIT_URL_GIT = 'git://pagure.io/'
|
||||
|
||||
### Folder containing to the git repos
|
||||
GIT_FOLDER = os.path.join(
|
||||
os.path.abspath(os.path.dirname(__file__)),
|
||||
'..',
|
||||
'repos'
|
||||
)
|
||||
|
||||
### Folder containing the forks repos
|
||||
FORK_FOLDER = os.path.join(
|
||||
os.path.abspath(os.path.dirname(__file__)),
|
||||
'..',
|
||||
'forks'
|
||||
)
|
||||
|
||||
### Folder containing the docs repos
|
||||
DOCS_FOLDER = os.path.join(
|
||||
os.path.abspath(os.path.dirname(__file__)),
|
||||
'..',
|
||||
'docs'
|
||||
)
|
||||
|
||||
### Folder containing the tickets repos
|
||||
TICKETS_FOLDER = os.path.join(
|
||||
os.path.abspath(os.path.dirname(__file__)),
|
||||
'..',
|
||||
'tickets'
|
||||
)
|
||||
|
||||
### Folder containing the pull-requests repos
|
||||
REQUESTS_FOLDER = os.path.join(
|
||||
os.path.abspath(os.path.dirname(__file__)),
|
||||
'..',
|
||||
'requests'
|
||||
)
|
||||
|
||||
### Folder containing the clones for the remote pull-requests
|
||||
REMOTE_GIT_FOLDER = os.path.join(
|
||||
os.path.abspath(os.path.dirname(__file__)),
|
||||
'..',
|
||||
'remotes'
|
||||
)
|
||||
|
||||
|
||||
### Configuration file for gitolite
|
||||
GITOLITE_CONFIG = os.path.join(
|
||||
os.path.abspath(os.path.dirname(__file__)),
|
||||
'..',
|
||||
'gitolite.conf'
|
||||
)
|
||||
|
||||
|
||||
### Home folder of the gitolite user
|
||||
### Folder where to run gl-compile-conf from
|
||||
GITOLITE_HOME = None
|
||||
|
||||
### Version of gitolite used: 2 or 3?
|
||||
GITOLITE_VERSION = 3
|
||||
|
||||
### Folder containing all the public ssh keys for gitolite
|
||||
GITOLITE_KEYDIR = None
|
||||
|
||||
### Path to the gitolite.rc file
|
||||
GL_RC = None
|
||||
|
||||
### Path to the /bin directory where the gitolite tools can be found
|
||||
GL_BINDIR = None
|
||||
|
||||
|
||||
# SSH Information
|
||||
|
||||
### The ssh certificates of the git server to be provided to the user
|
||||
### /!\ format is important
|
||||
# SSH_KEYS = {'RSA': {'fingerprint': '<foo>', 'pubkey': '<bar>'}}
|
||||
|
||||
|
||||
|
||||
# Optional configuration
|
||||
|
||||
### Number of items displayed per page
|
||||
# Used when listing items
|
||||
ITEM_PER_PAGE = 50
|
||||
|
||||
### Maximum size of the uploaded content
|
||||
# Used to limit the size of file attached to a ticket for example
|
||||
MAX_CONTENT_LENGTH = 4 * 1024 * 1024 # 4 megabytes
|
||||
|
||||
### Lenght for short commits ids or file hex
|
||||
SHORT_LENGTH = 6
|
||||
|
||||
### List of blacklisted project names that can conflicts for pagure's URLs
|
||||
### or other
|
||||
BLACKLISTED_PROJECTS = [
|
||||
'static', 'pv', 'releases', 'new', 'api', 'settings',
|
||||
'logout', 'login', 'users', 'groups', 'projects']
|
||||
|
||||
### IP addresses allowed to access the internal endpoints
|
||||
### These endpoints are used by the milter and are security sensitive, thus
|
||||
### the IP filter
|
||||
IP_ALLOWED_INTERNAL = ['127.0.0.1', 'localhost', '::1']
|
||||
|
||||
### EventSource/Web-Hook/Redis configuration
|
||||
# The eventsource integration is what allows pagure to refresh the content
|
||||
# on your page when someone else comments on the ticket (and this without
|
||||
# asking you to reload the page.
|
||||
# By default it is off, ie: EVENTSOURCE_SOURCE is None, to turn it on, specify
|
||||
# here what the URL of the eventsource server is, for example:
|
||||
# https://ev.pagure.io or https://pagure.io:8080 or whatever you are using
|
||||
# (Note: the urls sent to it start with a '/' so no need to add one yourself)
|
||||
EVENTSOURCE_SOURCE = None
|
||||
# Port where the event source server is running (maybe be the same port
|
||||
# as the one specified in EVENTSOURCE_SOURCE or a different one if you
|
||||
# have something running in front of the server such as apache or stunnel).
|
||||
EVENTSOURCE_PORT = 8080
|
||||
# If this port is specified, the event source server will run another server
|
||||
# at this port and will provide information about the number of active
|
||||
# connections running on the first (main) event source server
|
||||
#EV_STATS_PORT = 8888
|
||||
# Web-hook can be turned on or off allowing using them for notifications, or
|
||||
# not.
|
||||
WEBHOOK = False
|
||||
|
||||
### Redis configuration
|
||||
# A redis server is required for both the Event-Source server or the web-hook
|
||||
# server.
|
||||
REDIS_HOST = '0.0.0.0'
|
||||
REDIS_PORT = 6379
|
||||
REDIS_DB = 0
|
||||
|
||||
# Authentication related configuration option
|
||||
|
||||
### Switch the authentication method
|
||||
# Specify which authentication method to use, defaults to `fas` can be or
|
||||
# `local`
|
||||
# Default: ``fas``.
|
||||
PAGURE_AUTH = 'fas'
|
||||
|
||||
# When this is set to True, the session cookie will only be returned to the
|
||||
# server via ssl (https). If you connect to the server via plain http, the
|
||||
# cookie will not be sent. This prevents sniffing of the cookie contents.
|
||||
# This may be set to False when testing your application but should always
|
||||
# be set to True in production.
|
||||
# Default: ``True``.
|
||||
SESSION_COOKIE_SECURE = False
|
||||
|
||||
# The name of the cookie used to store the session id.
|
||||
# Default: ``.pagure``.
|
||||
SESSION_COOKIE_NAME = 'pagure'
|
||||
|
||||
# Boolean specifying whether to check the user's IP address when retrieving
|
||||
# its session. This make things more secure (thus is on by default) but
|
||||
# under certain setup it might not work (for example is there are proxies
|
||||
# in front of the application).
|
||||
CHECK_SESSION_IP = True
|
||||
|
||||
# Used by SESSION_COOKIE_PATH
|
||||
APPLICATION_ROOT = '/'
|
||||
|
||||
# Allow the backward compatiblity endpoints for the old URLs schema to
|
||||
# see the commits of a repo. This is only interesting if you pagure instance
|
||||
# was running since before version 1.3 and if you care about backward
|
||||
# compatibility in your URLs.
|
||||
OLD_VIEW_COMMIT_ENABLED = False
|
114
files/pagure.conf
Normal file
|
@ -0,0 +1,114 @@
|
|||
#WSGISocketPrefix run/wsgi
|
||||
##WSGIRestrictStdout On
|
||||
#WSGIRestrictSignal Off
|
||||
#WSGIPythonOptimize 1
|
||||
#WSGIPassAuthorization On
|
||||
#WSGIDaemonProcess pagure user=git group=git maximum-requests=1000 display-name=pagure processes=4 threads=4 inactivity-timeout=300
|
||||
## It is important that the doc server runs in a different apache process
|
||||
#WSGIDaemonProcess paguredocs user=git group=git maximum-requests=1000 display-name=pagure processes=4 threads=4 inactivity-timeout=300
|
||||
|
||||
#<VirtualHost *:80>
|
||||
#ServerName pagure.io
|
||||
#Redirect permanent / https://pagure.io/
|
||||
#</VirtualHost>
|
||||
|
||||
|
||||
#<VirtualHost *:80>
|
||||
#ServerName docs.pagure.org
|
||||
#Redirect permanent / https://docs.pagure.org/
|
||||
#</VirtualHost>
|
||||
|
||||
|
||||
#<VirtualHost *:443>
|
||||
#ServerName docs.pagure.org
|
||||
|
||||
#WSGIScriptAlias / /usr/share/pagure/docs_pagure.wsgi
|
||||
|
||||
#SSLEngine on
|
||||
#SSLProtocol all -SSLv2 -SSLv3
|
||||
## Use secure TLSv1.1 and TLSv1.2 ciphers
|
||||
#Header always add Strict-Transport-Security "max-age=15768000; includeSubDomains; preload"
|
||||
|
||||
#SSLCertificateFile /etc/pki/tls/....crt
|
||||
#SSLCertificateChainFile /etc/pki/tls/....intermediate.crt
|
||||
#SSLCertificateKeyFile /etc/pki/tls/....key
|
||||
|
||||
#Alias /static /usr/lib/python2.7/site-packages/pagure/static/
|
||||
|
||||
#<Location />
|
||||
#WSGIProcessGroup paguredocs
|
||||
#<IfModule mod_authz_core.c>
|
||||
## Apache 2.4
|
||||
#Require all granted
|
||||
#</IfModule>
|
||||
#<IfModule !mod_authz_core.c>
|
||||
## Apache 2.2
|
||||
#Order deny,allow
|
||||
#Allow from all
|
||||
#</IfModule>
|
||||
#</Location>
|
||||
#</VirtualHost>
|
||||
|
||||
|
||||
#<VirtualHost *:443>
|
||||
#ServerName pagure.io
|
||||
|
||||
#WSGIScriptAlias / /usr/share/pagure/pagure.wsgi
|
||||
|
||||
#SSLEngine on
|
||||
#SSLProtocol all -SSLv2 -SSLv3
|
||||
## Use secure TLSv1.1 and TLSv1.2 ciphers
|
||||
#Header always add Strict-Transport-Security "max-age=15768000; includeSubDomains; preload"
|
||||
|
||||
#SSLCertificateFile /etc/pki/tls/....crt
|
||||
#SSLCertificateChainFile /etc/pki/tls/....intermediate.crt
|
||||
#SSLCertificateKeyFile /etc/pki/tls/....key
|
||||
|
||||
#Alias /static /usr/lib/python2.7/site-packages/pagure/static/
|
||||
#Alias /releases /var/www/releases
|
||||
|
||||
## Section used to support cloning git repo over http (https in this case)
|
||||
#SetEnv GIT_PROJECT_ROOT /srv/git/repositories
|
||||
|
||||
#AliasMatch ^/(.*/objects/[0-9a-f]{2}/[0-9a-f]{38})$ /path/to/git/repositories/$1
|
||||
#AliasMatch ^/(.*/objects/pack/pack-[0-9a-f]{40}.(pack|idx))$ /path/to/git/repositories/$1
|
||||
#ScriptAliasMatch \
|
||||
#"(?x)^/(.*/(HEAD | \
|
||||
#info/refs | \
|
||||
#objects/info/[^/]+ | \
|
||||
#git-(upload|receive)-pack))$" \
|
||||
#/usr/libexec/git-core/git-http-backend/$1
|
||||
|
||||
#<Location />
|
||||
#WSGIProcessGroup pagure
|
||||
#<IfModule mod_authz_core.c>
|
||||
## Apache 2.4
|
||||
#Require all granted
|
||||
#</IfModule>
|
||||
#<IfModule !mod_authz_core.c>
|
||||
## Apache 2.2
|
||||
#Order deny,allow
|
||||
#Allow from all
|
||||
#</IfModule>
|
||||
#</Location>
|
||||
|
||||
## Folder where are stored the tarball of the releases
|
||||
#<Location /releases>
|
||||
#WSGIProcessGroup pagure
|
||||
#<IfModule mod_authz_core.c>
|
||||
## Apache 2.4
|
||||
#Require all granted
|
||||
#</IfModule>
|
||||
#<IfModule !mod_authz_core.c>
|
||||
## Apache 2.2
|
||||
#Order deny,allow
|
||||
#Allow from all
|
||||
#</IfModule>
|
||||
#</Location>
|
||||
|
||||
#<Directory /var/www/releases>
|
||||
#Options +Indexes
|
||||
#</Directory>
|
||||
|
||||
#</VirtualHost>
|
||||
|
1083
files/pagure.spec
Normal file
28
files/pagure.wsgi
Normal file
|
@ -0,0 +1,28 @@
|
|||
#-*- coding: utf-8 -*-
|
||||
|
||||
# The three lines below are required to run on EL6 as EL6 has
|
||||
# two possible version of python-sqlalchemy and python-jinja2
|
||||
# These lines make sure the application uses the correct version.
|
||||
import __main__
|
||||
__main__.__requires__ = ['SQLAlchemy >= 0.8', 'jinja2 >= 2.4']
|
||||
import pkg_resources
|
||||
|
||||
# Set the environment variable pointing to the configuration file
|
||||
import os
|
||||
os.environ['PAGURE_CONFIG'] = '/etc/pagure/pagure.cfg'
|
||||
|
||||
# Set the environment variable if the tmp folder needs to be moved
|
||||
# Might be necessary to work around bug in libgit2:
|
||||
# refs: https://github.com/libgit2/libgit2/issues/2965
|
||||
# and https://github.com/libgit2/libgit2/issues/2797
|
||||
os.environ['TEMP'] = '/var/tmp/'
|
||||
|
||||
# The following is only needed if you did not install pagure
|
||||
# as a python module (for example if you run it from a git clone).
|
||||
#import sys
|
||||
#sys.path.insert(0, '/path/to/pagure/')
|
||||
|
||||
|
||||
# The most import line to make the wsgi working
|
||||
from pagure import APP as application
|
||||
#application.debug = True
|
247
milters/comment_email_milter.py
Normal file
|
@ -0,0 +1,247 @@
|
|||
#!/usr/bin/env python2
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Milter calls methods of your class at milter events.
|
||||
# Return REJECT,TEMPFAIL,ACCEPT to short circuit processing for a message.
|
||||
# You can also add/del recipients, replacebody, add/del headers, etc.
|
||||
|
||||
import base64
|
||||
import email
|
||||
import hashlib
|
||||
import os
|
||||
import urlparse
|
||||
import StringIO
|
||||
import sys
|
||||
import time
|
||||
from socket import AF_INET, AF_INET6
|
||||
from multiprocessing import Process as Thread, Queue
|
||||
|
||||
import Milter
|
||||
import requests
|
||||
|
||||
from Milter.utils import parse_addr
|
||||
|
||||
logq = Queue(maxsize=4)
|
||||
|
||||
|
||||
if 'PAGURE_CONFIG' not in os.environ \
|
||||
and os.path.exists('/etc/pagure/pagure.cfg'):
|
||||
os.environ['PAGURE_CONFIG'] = '/etc/pagure/pagure.cfg'
|
||||
|
||||
|
||||
import pagure
|
||||
|
||||
|
||||
def get_email_body(emailobj):
|
||||
''' Return the body of the email, preferably in text.
|
||||
'''
|
||||
body = None
|
||||
if emailobj.is_multipart():
|
||||
for payload in emailobj.get_payload():
|
||||
body = payload.get_payload()
|
||||
if payload.get_content_type() == 'text/plain':
|
||||
break
|
||||
else:
|
||||
body = emailobj.get_payload()
|
||||
|
||||
enc = emailobj['Content-Transfer-Encoding']
|
||||
if enc == 'base64':
|
||||
body = base64.decodestring(body)
|
||||
|
||||
return body
|
||||
|
||||
|
||||
def clean_item(item):
|
||||
''' For an item provided as <item> return the content, if there are no
|
||||
<> then return the string.
|
||||
'''
|
||||
if '<' in item:
|
||||
item = item.split('<')[1]
|
||||
if '>' in item:
|
||||
item = item.split('>')[0]
|
||||
|
||||
return item
|
||||
|
||||
|
||||
class PagureMilter(Milter.Base):
|
||||
|
||||
def __init__(self): # A new instance with each new connection.
|
||||
self.id = Milter.uniqueID() # Integer incremented with each call.
|
||||
self.fp = None
|
||||
|
||||
def log(self, message):
|
||||
print(message)
|
||||
sys.stdout.flush()
|
||||
|
||||
def envfrom(self, mailfrom, *str):
|
||||
self.log("mail from: %s - %s" % (mailfrom, str))
|
||||
self.fromparms = Milter.dictfromlist(str)
|
||||
# NOTE: self.fp is only an *internal* copy of message data. You
|
||||
# must use addheader, chgheader, replacebody to change the message
|
||||
# on the MTA.
|
||||
self.fp = StringIO.StringIO()
|
||||
self.canon_from = '@'.join(parse_addr(mailfrom))
|
||||
self.fp.write('From %s %s\n' % (self.canon_from, time.ctime()))
|
||||
return Milter.CONTINUE
|
||||
|
||||
@Milter.noreply
|
||||
def header(self, name, hval):
|
||||
''' Headers '''
|
||||
# add header to buffer
|
||||
self.fp.write("%s: %s\n" % (name, hval))
|
||||
return Milter.CONTINUE
|
||||
|
||||
@Milter.noreply
|
||||
def eoh(self):
|
||||
''' End of Headers '''
|
||||
self.fp.write("\n")
|
||||
return Milter.CONTINUE
|
||||
|
||||
@Milter.noreply
|
||||
def body(self, chunk):
|
||||
''' Body '''
|
||||
self.fp.write(chunk)
|
||||
return Milter.CONTINUE
|
||||
|
||||
@Milter.noreply
|
||||
def envrcpt(self, to, *str):
|
||||
rcptinfo = to, Milter.dictfromlist(str)
|
||||
print rcptinfo
|
||||
|
||||
return Milter.CONTINUE
|
||||
|
||||
def eom(self):
|
||||
''' End of Message '''
|
||||
self.fp.seek(0)
|
||||
msg = email.message_from_file(self.fp)
|
||||
|
||||
msg_id = msg.get('In-Reply-To', None)
|
||||
if msg_id is None:
|
||||
self.log('No In-Reply-To, keep going')
|
||||
return Milter.CONTINUE
|
||||
|
||||
# Ensure we don't get extra lines in the message-id
|
||||
msg_id = msg_id.split('\n')[0].strip()
|
||||
|
||||
self.log('msg-ig %s' % msg_id)
|
||||
self.log('To %s' % msg['to'])
|
||||
self.log('Cc %s' % msg.get('cc'))
|
||||
self.log('From %s' % msg['From'])
|
||||
|
||||
# Ensure the user replied to his/her own notification, not that
|
||||
# they are trying to forge their ID into someone else's
|
||||
salt = pagure.APP.config.get('SALT_EMAIL')
|
||||
m = hashlib.sha512('%s%s%s' % (msg_id, salt, clean_item(msg['From'])))
|
||||
email_address = msg['to']
|
||||
if 'reply+' in msg.get('cc', ''):
|
||||
email_address = msg['cc']
|
||||
if not 'reply+' in email_address:
|
||||
self.log(
|
||||
'No valid recipient email found in To/Cc: %s'
|
||||
% email_address)
|
||||
tohash = email_address.split('@')[0].split('+')[-1]
|
||||
if m.hexdigest() != tohash:
|
||||
self.log('hash: %s' % m.hexdigest())
|
||||
self.log('tohash: %s' % tohash)
|
||||
self.log('Hash does not correspond to the destination')
|
||||
return Milter.CONTINUE
|
||||
|
||||
if msg['From'] and msg['From'] == pagure.APP.config.get('FROM_EMAIL'):
|
||||
self.log("Let's not process the email we send")
|
||||
return Milter.CONTINUE
|
||||
|
||||
msg_id = clean_item(msg_id)
|
||||
|
||||
if msg_id and '-ticket-' in msg_id:
|
||||
self.log('Processing issue')
|
||||
return self.handle_ticket_email(msg, msg_id)
|
||||
elif msg_id and '-pull-request-' in msg_id:
|
||||
self.log('Processing pull-request')
|
||||
return self.handle_request_email(msg, msg_id)
|
||||
else:
|
||||
self.log('Not a pagure ticket or pull-request email, let it go')
|
||||
return Milter.CONTINUE
|
||||
|
||||
|
||||
def handle_ticket_email(self, emailobj, msg_id):
|
||||
''' Add the email as a comment on a ticket. '''
|
||||
uid = msg_id.split('-ticket-')[-1].split('@')[0]
|
||||
parent_id = None
|
||||
if '-' in uid:
|
||||
uid, parent_id = uid.rsplit('-', 1)
|
||||
if '/' in uid:
|
||||
uid = uid.split('/')[0]
|
||||
self.log('uid %s' % uid)
|
||||
self.log('parent_id %s' % parent_id)
|
||||
|
||||
data = {
|
||||
'objid': uid,
|
||||
'comment': get_email_body(emailobj),
|
||||
'useremail': clean_item(emailobj['From']),
|
||||
}
|
||||
url = pagure.APP.config.get('APP_URL')
|
||||
|
||||
if url.endswith('/'):
|
||||
url = url[:-1]
|
||||
url = '%s/pv/ticket/comment/' % url
|
||||
req = requests.put(url, data=data)
|
||||
if req.status_code == 200:
|
||||
self.log('Comment added')
|
||||
return Milter.ACCEPT
|
||||
self.log('Could not add the comment to pagure')
|
||||
return Milter.CONTINUE
|
||||
|
||||
def handle_request_email(self, emailobj, msg_id):
|
||||
''' Add the email as a comment on a request. '''
|
||||
uid = msg_id.split('-pull-request-')[-1].split('@')[0]
|
||||
parent_id = None
|
||||
if '-' in uid:
|
||||
uid, parent_id = uid.rsplit('-', 1)
|
||||
if '/' in uid:
|
||||
uid = uid.split('/')[0]
|
||||
self.log('uid %s' % uid)
|
||||
self.log('parent_id %s' % parent_id)
|
||||
|
||||
data = {
|
||||
'objid': uid,
|
||||
'comment': get_email_body(emailobj),
|
||||
'useremail': clean_item(emailobj['From']),
|
||||
}
|
||||
url = pagure.APP.config.get('APP_URL')
|
||||
|
||||
if url.endswith('/'):
|
||||
url = url[:-1]
|
||||
url = '%s/pv/pull-request/comment/' % url
|
||||
req = requests.put(url, data=data)
|
||||
|
||||
return Milter.ACCEPT
|
||||
|
||||
|
||||
def background():
|
||||
while True:
|
||||
t = logq.get()
|
||||
if not t: break
|
||||
msg,id,ts = t
|
||||
print "%s [%d]" % (time.strftime('%Y%b%d %H:%M:%S',time.localtime(ts)),id),
|
||||
# 2005Oct13 02:34:11 [1] msg1 msg2 msg3 ...
|
||||
for i in msg: print i,
|
||||
print
|
||||
|
||||
|
||||
def main():
|
||||
bt = Thread(target=background)
|
||||
bt.start()
|
||||
socketname = "/var/run/pagure/paguresock"
|
||||
timeout = 600
|
||||
# Register to have the Milter factory create instances of your class:
|
||||
Milter.factory = PagureMilter
|
||||
print "%s pagure milter startup" % time.strftime('%Y%b%d %H:%M:%S')
|
||||
sys.stdout.flush()
|
||||
Milter.runmilter("paguremilter", socketname, timeout)
|
||||
logq.put(None)
|
||||
bt.join()
|
||||
print "%s pagure milter shutdown" % time.strftime('%Y%b%d %H:%M:%S')
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
1
milters/milter_tempfile.conf
Normal file
|
@ -0,0 +1 @@
|
|||
d /var/run/pagure 0755 postfix postfix
|
14
milters/pagure_milter.service
Normal file
|
@ -0,0 +1,14 @@
|
|||
[Unit]
|
||||
Description=Pagure SMTP filter (Milter) Daemon (talk to postfix over a socket)
|
||||
After=postfix.target
|
||||
Documentation=https://github.com/pypingou/pagure
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/bin/python2 /usr/share/pagure/comment_email_milter.py
|
||||
Type=simple
|
||||
User=postfix
|
||||
Group=postfix
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
11
pagure.egg-info/PKG-INFO
Normal file
|
@ -0,0 +1,11 @@
|
|||
Metadata-Version: 1.1
|
||||
Name: pagure
|
||||
Version: 2.3.4
|
||||
Summary: A light-weight git-centered forge based on pygit2..
|
||||
Home-page: https://fedorahosted.org/pagure/
|
||||
Author: Pierre-Yves Chibon
|
||||
Author-email: pingou@pingoured.fr
|
||||
License: GPLv2+
|
||||
Download-URL: https://fedorahosted.org/releases/p/r/pagure/
|
||||
Description: UNKNOWN
|
||||
Platform: UNKNOWN
|
340
pagure.egg-info/SOURCES.txt
Normal file
|
@ -0,0 +1,340 @@
|
|||
LICENSE
|
||||
MANIFEST.in
|
||||
README.rst
|
||||
UPGRADING.rst
|
||||
createdb.py
|
||||
requirements.txt
|
||||
setup.py
|
||||
alembic/env.py
|
||||
alembic/script.py.mako
|
||||
alembic/versions/15ea3c2cf83d_pr_comment_editing.py
|
||||
alembic/versions/1b6d7dc5600a_versioning_passwords.py
|
||||
alembic/versions/1cd0a853c697_add_closed_at_field_in_pr.py
|
||||
alembic/versions/1f3de3853a1a_add_the_tree_id_column_to_pr_inline_.py
|
||||
alembic/versions/22db0a833d35_add_notifications_to_tickets.py
|
||||
alembic/versions/257a7ce22682_add_the_remote_git_entry.py
|
||||
alembic/versions/298891e63039_change_the_status_of_pull_requests.py
|
||||
alembic/versions/2aa7b3958bc5_add_the_milestones_column.py
|
||||
alembic/versions/317a285e04a8_delete_hooks.py
|
||||
alembic/versions/36116bb7a69b_add_the_url_field_to_project.py
|
||||
alembic/versions/3b441ef4e928_comment_editing_issue.py
|
||||
alembic/versions/3c25e14b855b_add_an_avatar_email_for_project.py
|
||||
alembic/versions/43df5e588a87_add_closed_at_attribute_in_issues.py
|
||||
alembic/versions/443e090da188_up_to_255_characters_for_project_name.py
|
||||
alembic/versions/496f7a700f2e_add_priorities.py
|
||||
alembic/versions/4cae55a80a42_add_the_initial_comment_on_the_pr_table.py
|
||||
alembic/versions/58e60d869326_add_notification_bool_to_pr.py
|
||||
alembic/versions/6190226bed0_add_the_updated_on_column_to_pull_.py
|
||||
alembic/versions/abc71fd60fa_add_the_closed_by_column_to_pull_.py
|
||||
alembic/versions/b5efae6bb23_add_merge_status_to_the_pull_requests_.py
|
||||
doc/Makefile
|
||||
doc/api.rst
|
||||
doc/conf.py
|
||||
doc/configuration.rst
|
||||
doc/contributing.rst
|
||||
doc/contributors.rst
|
||||
doc/development.rst
|
||||
doc/index.rst
|
||||
doc/install.rst
|
||||
doc/install_evs.rst
|
||||
doc/install_milter.rst
|
||||
doc/install_webhooks.rst
|
||||
doc/milter.rst
|
||||
doc/overview.ascii
|
||||
doc/overview.rst
|
||||
doc/overview_simple.ascii
|
||||
doc/requirements.txt
|
||||
doc/usage.rst
|
||||
doc/_static/overview.png
|
||||
doc/_static/overview_simple.png
|
||||
doc/_static/pagure.png
|
||||
doc/_static/site.css
|
||||
doc/_templates/pagure-logo.html
|
||||
doc/usage/first_steps.rst
|
||||
doc/usage/pr_custom_page.rst
|
||||
doc/usage/project_settings.rst
|
||||
doc/usage/roadmap.rst
|
||||
doc/usage/theming.rst
|
||||
doc/usage/ticket_templates.rst
|
||||
doc/usage/upgrade_db.rst
|
||||
doc/usage/using_doc.rst
|
||||
doc/usage/using_webhooks.rst
|
||||
doc/usage/_static/pagure_custom_pr.png
|
||||
doc/usage/_static/pagure_my_settings.png
|
||||
doc/usage/_static/pagure_roadmap2.png
|
||||
doc/usage/_static/pagure_ticket_template.png
|
||||
ev-server/pagure-stream-server.py
|
||||
ev-server/pagure_ev.service
|
||||
files/alembic.ini
|
||||
files/api_key_expire_mail.py
|
||||
files/doc_pagure.wsgi
|
||||
files/emoji_clean_json.py
|
||||
files/gitolite.rc
|
||||
files/gitolite3.rc
|
||||
files/load_from_disk.py
|
||||
files/pagure.cfg.sample
|
||||
files/pagure.conf
|
||||
files/pagure.spec
|
||||
files/pagure.wsgi
|
||||
milters/comment_email_milter.py
|
||||
milters/milter_tempfile.conf
|
||||
milters/pagure_milter.service
|
||||
pagure/__init__.py
|
||||
pagure/default_config.py
|
||||
pagure/doc_utils.py
|
||||
pagure/docs_server.py
|
||||
pagure/exceptions.py
|
||||
pagure/forms.py
|
||||
pagure/login_forms.py
|
||||
pagure/mail_logging.py
|
||||
pagure/pfmarkdown.py
|
||||
pagure/proxy.py
|
||||
pagure.egg-info/PKG-INFO
|
||||
pagure.egg-info/SOURCES.txt
|
||||
pagure.egg-info/dependency_links.txt
|
||||
pagure.egg-info/requires.txt
|
||||
pagure.egg-info/top_level.txt
|
||||
pagure/api/__init__.py
|
||||
pagure/api/fork.py
|
||||
pagure/api/issue.py
|
||||
pagure/api/project.py
|
||||
pagure/api/user.py
|
||||
pagure/doc/api.rst
|
||||
pagure/hooks/__init__.py
|
||||
pagure/hooks/fedmsg.py
|
||||
pagure/hooks/irc.py
|
||||
pagure/hooks/mail.py
|
||||
pagure/hooks/pagure_force_commit.py
|
||||
pagure/hooks/pagure_hook.py
|
||||
pagure/hooks/pagure_request_hook.py
|
||||
pagure/hooks/pagure_ticket_hook.py
|
||||
pagure/hooks/pagure_unsigned_commits.py
|
||||
pagure/hooks/rtd.py
|
||||
pagure/hooks/files/fedmsg_hook.py
|
||||
pagure/hooks/files/git_multimail.py
|
||||
pagure/hooks/files/pagure_block_unsigned.py
|
||||
pagure/hooks/files/pagure_force_commit_hook.py
|
||||
pagure/hooks/files/pagure_hook.py
|
||||
pagure/hooks/files/pagure_hook_requests.py
|
||||
pagure/hooks/files/pagure_hook_tickets.py
|
||||
pagure/hooks/files/post-receive
|
||||
pagure/hooks/files/pre-receive
|
||||
pagure/hooks/files/rtd_hook.py
|
||||
pagure/internal/__init__.py
|
||||
pagure/lib/__init__.py
|
||||
pagure/lib/git.py
|
||||
pagure/lib/link.py
|
||||
pagure/lib/login.py
|
||||
pagure/lib/model.py
|
||||
pagure/lib/notify.py
|
||||
pagure/lib/repo.py
|
||||
pagure/static/favicon.ico
|
||||
pagure/static/issue_ev.js
|
||||
pagure/static/jquery-1.10.2.js
|
||||
pagure/static/jquery-ui-1.11.2.custom.min.js
|
||||
pagure/static/jquery.dotdotdot.min.js
|
||||
pagure/static/pagure-logo.png
|
||||
pagure/static/pagure.css
|
||||
pagure/static/request_ev.js
|
||||
pagure/static/selectize.bootstrap3.css
|
||||
pagure/static/selectize.min.js
|
||||
pagure/static/stupidtable.min.js
|
||||
pagure/static/toggle.css
|
||||
pagure/static/upload.js
|
||||
pagure/static/atwho/jquery.atwho.min.css
|
||||
pagure/static/atwho/jquery.atwho.min.js
|
||||
pagure/static/atwho/jquery.caret.min.js
|
||||
pagure/static/codemirror/codemirror.css
|
||||
pagure/static/codemirror/codemirror.js
|
||||
pagure/static/codemirror/solarized.css
|
||||
pagure/static/emoji/emoji_strategy.json
|
||||
pagure/static/emoji/emojione.min.js
|
||||
pagure/static/emoji/emojione.sprites.css
|
||||
pagure/static/emoji/emojione.sprites.png
|
||||
pagure/static/emoji/jquery.textcomplete.min.js
|
||||
pagure/static/fonts/fonts.css
|
||||
pagure/static/fonts/open-sans-v13-latin_latin-ext-300.eot
|
||||
pagure/static/fonts/open-sans-v13-latin_latin-ext-300.svg
|
||||
pagure/static/fonts/open-sans-v13-latin_latin-ext-300.ttf
|
||||
pagure/static/fonts/open-sans-v13-latin_latin-ext-300.woff
|
||||
pagure/static/fonts/open-sans-v13-latin_latin-ext-300.woff2
|
||||
pagure/static/fonts/open-sans-v13-latin_latin-ext-300italic.eot
|
||||
pagure/static/fonts/open-sans-v13-latin_latin-ext-300italic.svg
|
||||
pagure/static/fonts/open-sans-v13-latin_latin-ext-300italic.ttf
|
||||
pagure/static/fonts/open-sans-v13-latin_latin-ext-300italic.woff
|
||||
pagure/static/fonts/open-sans-v13-latin_latin-ext-300italic.woff2
|
||||
pagure/static/fonts/open-sans-v13-latin_latin-ext-700.eot
|
||||
pagure/static/fonts/open-sans-v13-latin_latin-ext-700.svg
|
||||
pagure/static/fonts/open-sans-v13-latin_latin-ext-700.ttf
|
||||
pagure/static/fonts/open-sans-v13-latin_latin-ext-700.woff
|
||||
pagure/static/fonts/open-sans-v13-latin_latin-ext-700.woff2
|
||||
pagure/static/fonts/open-sans-v13-latin_latin-ext-700italic.eot
|
||||
pagure/static/fonts/open-sans-v13-latin_latin-ext-700italic.svg
|
||||
pagure/static/fonts/open-sans-v13-latin_latin-ext-700italic.ttf
|
||||
pagure/static/fonts/open-sans-v13-latin_latin-ext-700italic.woff
|
||||
pagure/static/fonts/open-sans-v13-latin_latin-ext-700italic.woff2
|
||||
pagure/static/fonts/open-sans-v13-latin_latin-ext-italic.eot
|
||||
pagure/static/fonts/open-sans-v13-latin_latin-ext-italic.svg
|
||||
pagure/static/fonts/open-sans-v13-latin_latin-ext-italic.ttf
|
||||
pagure/static/fonts/open-sans-v13-latin_latin-ext-italic.woff
|
||||
pagure/static/fonts/open-sans-v13-latin_latin-ext-italic.woff2
|
||||
pagure/static/fonts/open-sans-v13-latin_latin-ext-regular.eot
|
||||
pagure/static/fonts/open-sans-v13-latin_latin-ext-regular.svg
|
||||
pagure/static/fonts/open-sans-v13-latin_latin-ext-regular.ttf
|
||||
pagure/static/fonts/open-sans-v13-latin_latin-ext-regular.woff
|
||||
pagure/static/fonts/open-sans-v13-latin_latin-ext-regular.woff2
|
||||
pagure/static/hack_fonts/css/hack-extended.min.css
|
||||
pagure/static/hack_fonts/fonts/eot/hack-bold-webfont.eot
|
||||
pagure/static/hack_fonts/fonts/eot/hack-bolditalic-webfont.eot
|
||||
pagure/static/hack_fonts/fonts/eot/hack-italic-webfont.eot
|
||||
pagure/static/hack_fonts/fonts/eot/hack-regular-webfont.eot
|
||||
pagure/static/hack_fonts/fonts/eot/latin/hack-bold-latin-webfont.eot
|
||||
pagure/static/hack_fonts/fonts/eot/latin/hack-bolditalic-latin-webfont.eot
|
||||
pagure/static/hack_fonts/fonts/eot/latin/hack-italic-latin-webfont.eot
|
||||
pagure/static/hack_fonts/fonts/eot/latin/hack-regular-latin-webfont.eot
|
||||
pagure/static/hack_fonts/fonts/svg/hack-bold-webfont.svg
|
||||
pagure/static/hack_fonts/fonts/svg/hack-bolditalic-webfont.svg
|
||||
pagure/static/hack_fonts/fonts/svg/hack-italic-webfont.svg
|
||||
pagure/static/hack_fonts/fonts/svg/hack-regular-webfont.svg
|
||||
pagure/static/hack_fonts/fonts/svg/latin/hack-bold-latin-webfont.svg
|
||||
pagure/static/hack_fonts/fonts/svg/latin/hack-bolditalic-latin-webfont.svg
|
||||
pagure/static/hack_fonts/fonts/svg/latin/hack-italic-latin-webfont.svg
|
||||
pagure/static/hack_fonts/fonts/svg/latin/hack-regular-latin-webfont.svg
|
||||
pagure/static/hack_fonts/fonts/web-ttf/hack-bold-webfont.ttf
|
||||
pagure/static/hack_fonts/fonts/web-ttf/hack-bolditalic-webfont.ttf
|
||||
pagure/static/hack_fonts/fonts/web-ttf/hack-italic-webfont.ttf
|
||||
pagure/static/hack_fonts/fonts/web-ttf/hack-regular-webfont.ttf
|
||||
pagure/static/hack_fonts/fonts/web-ttf/latin/hack-bold-latin-webfont.ttf
|
||||
pagure/static/hack_fonts/fonts/web-ttf/latin/hack-bolditalic-latin-webfont.ttf
|
||||
pagure/static/hack_fonts/fonts/web-ttf/latin/hack-italic-latin-webfont.ttf
|
||||
pagure/static/hack_fonts/fonts/web-ttf/latin/hack-regular-latin-webfont.ttf
|
||||
pagure/static/hack_fonts/fonts/woff/hack-bold-webfont.woff
|
||||
pagure/static/hack_fonts/fonts/woff/hack-bolditalic-webfont.woff
|
||||
pagure/static/hack_fonts/fonts/woff/hack-italic-webfont.woff
|
||||
pagure/static/hack_fonts/fonts/woff/hack-regular-webfont.woff
|
||||
pagure/static/hack_fonts/fonts/woff/latin/hack-bold-latin-webfont.woff
|
||||
pagure/static/hack_fonts/fonts/woff/latin/hack-bolditalic-latin-webfont.woff
|
||||
pagure/static/hack_fonts/fonts/woff/latin/hack-italic-latin-webfont.woff
|
||||
pagure/static/hack_fonts/fonts/woff/latin/hack-regular-latin-webfont.woff
|
||||
pagure/static/hack_fonts/fonts/woff2/hack-bold-webfont.woff2
|
||||
pagure/static/hack_fonts/fonts/woff2/hack-bolditalic-webfont.woff2
|
||||
pagure/static/hack_fonts/fonts/woff2/hack-italic-webfont.woff2
|
||||
pagure/static/hack_fonts/fonts/woff2/hack-regular-webfont.woff2
|
||||
pagure/static/hack_fonts/fonts/woff2/latin/hack-bold-latin-webfont.woff2
|
||||
pagure/static/hack_fonts/fonts/woff2/latin/hack-bolditalic-latin-webfont.woff2
|
||||
pagure/static/hack_fonts/fonts/woff2/latin/hack-italic-latin-webfont.woff2
|
||||
pagure/static/hack_fonts/fonts/woff2/latin/hack-regular-latin-webfont.woff2
|
||||
pagure/static/images/link.png
|
||||
pagure/static/images/spinner.gif
|
||||
pagure/static/images/users.png
|
||||
pagure/static/open_iconic_1.1.0/css/open-iconic.min.css
|
||||
pagure/static/open_iconic_1.1.0/fonts/open-iconic.eot
|
||||
pagure/static/open_iconic_1.1.0/fonts/open-iconic.otf
|
||||
pagure/static/open_iconic_1.1.0/fonts/open-iconic.svg
|
||||
pagure/static/open_iconic_1.1.0/fonts/open-iconic.ttf
|
||||
pagure/static/open_iconic_1.1.0/fonts/open-iconic.woff
|
||||
pagure/templates/_browseheader.html
|
||||
pagure/templates/_formhelper.html
|
||||
pagure/templates/_render_repo.html
|
||||
pagure/templates/activity.html
|
||||
pagure/templates/add_group.html
|
||||
pagure/templates/add_group_project.html
|
||||
pagure/templates/add_token.html
|
||||
pagure/templates/add_user.html
|
||||
pagure/templates/admin_index.html
|
||||
pagure/templates/api.html
|
||||
pagure/templates/comment_update.html
|
||||
pagure/templates/commit.html
|
||||
pagure/templates/commits.html
|
||||
pagure/templates/doc_ssh_keys.html
|
||||
pagure/templates/docs.html
|
||||
pagure/templates/edit_file.html
|
||||
pagure/templates/edit_tag.html
|
||||
pagure/templates/fatal_error.html
|
||||
pagure/templates/file.html
|
||||
pagure/templates/forks.html
|
||||
pagure/templates/group_info.html
|
||||
pagure/templates/group_list.html
|
||||
pagure/templates/index.html
|
||||
pagure/templates/index_auth.html
|
||||
pagure/templates/issue.html
|
||||
pagure/templates/issues.html
|
||||
pagure/templates/master.html
|
||||
pagure/templates/new_issue.html
|
||||
pagure/templates/new_project.html
|
||||
pagure/templates/new_release.html
|
||||
pagure/templates/not_found.html
|
||||
pagure/templates/plugin.html
|
||||
pagure/templates/pull_request.html
|
||||
pagure/templates/pull_request_comment.html
|
||||
pagure/templates/pull_request_title.html
|
||||
pagure/templates/releases.html
|
||||
pagure/templates/remote_pull_request.html
|
||||
pagure/templates/repo_info.html
|
||||
pagure/templates/repo_master.html
|
||||
pagure/templates/requests.html
|
||||
pagure/templates/roadmap.html
|
||||
pagure/templates/settings.html
|
||||
pagure/templates/unauthorized.html
|
||||
pagure/templates/user_emails.html
|
||||
pagure/templates/user_info.html
|
||||
pagure/templates/user_list.html
|
||||
pagure/templates/user_requests.html
|
||||
pagure/templates/user_settings.html
|
||||
pagure/templates/login/login.html
|
||||
pagure/templates/login/password_change.html
|
||||
pagure/templates/login/password_recover.html
|
||||
pagure/templates/login/password_reset.html
|
||||
pagure/templates/login/user_new.html
|
||||
pagure/ui/__init__.py
|
||||
pagure/ui/admin.py
|
||||
pagure/ui/app.py
|
||||
pagure/ui/filters.py
|
||||
pagure/ui/fork.py
|
||||
pagure/ui/groups.py
|
||||
pagure/ui/issues.py
|
||||
pagure/ui/login.py
|
||||
pagure/ui/plugins.py
|
||||
pagure/ui/repo.py
|
||||
tests/__init__.py
|
||||
tests/placebo.png
|
||||
tests/test_config
|
||||
tests/test_pagure_flask_api.py
|
||||
tests/test_pagure_flask_api_auth.py
|
||||
tests/test_pagure_flask_api_fork.py
|
||||
tests/test_pagure_flask_api_issue.py
|
||||
tests/test_pagure_flask_api_project.py
|
||||
tests/test_pagure_flask_docs.py
|
||||
tests/test_pagure_flask_dump_load_ticket.py
|
||||
tests/test_pagure_flask_internal.py
|
||||
tests/test_pagure_flask_ui_admin.py
|
||||
tests/test_pagure_flask_ui_app.py
|
||||
tests/test_pagure_flask_ui_fork.py
|
||||
tests/test_pagure_flask_ui_groups.py
|
||||
tests/test_pagure_flask_ui_issues.py
|
||||
tests/test_pagure_flask_ui_login.py
|
||||
tests/test_pagure_flask_ui_no_master_branch.py
|
||||
tests/test_pagure_flask_ui_plugins.py
|
||||
tests/test_pagure_flask_ui_plugins_fedmsg.py
|
||||
tests/test_pagure_flask_ui_plugins_irc.py
|
||||
tests/test_pagure_flask_ui_plugins_mail.py
|
||||
tests/test_pagure_flask_ui_plugins_noff.py
|
||||
tests/test_pagure_flask_ui_plugins_pagure_hook.py
|
||||
tests/test_pagure_flask_ui_plugins_pagure_request_hook.py
|
||||
tests/test_pagure_flask_ui_plugins_pagure_ticket_hook.py
|
||||
tests/test_pagure_flask_ui_plugins_rtd_hook.py
|
||||
tests/test_pagure_flask_ui_plugins_unsigned.py
|
||||
tests/test_pagure_flask_ui_priorities.py
|
||||
tests/test_pagure_flask_ui_repo.py
|
||||
tests/test_pagure_flask_ui_repo_slash_name.py
|
||||
tests/test_pagure_flask_ui_roadmap.py
|
||||
tests/test_pagure_flask_ui_slash_branch_name.py
|
||||
tests/test_pagure_lib.py
|
||||
tests/test_pagure_lib_git.py
|
||||
tests/test_pagure_lib_git_get_tags_objects.py
|
||||
tests/test_pagure_lib_link.py
|
||||
tests/test_pagure_lib_login.py
|
||||
tests/test_pagure_lib_model.py
|
||||
tests/test_zzz_pagure_flask_ui_old_commit.py
|
||||
webhook-server/pagure-webhook-server.py
|
||||
webhook-server/pagure_webhook.service
|
1
pagure.egg-info/dependency_links.txt
Normal file
|
@ -0,0 +1 @@
|
|||
|
31
pagure.egg-info/requires.txt
Normal file
|
@ -0,0 +1,31 @@
|
|||
alembic
|
||||
arrow
|
||||
binaryornot
|
||||
bleach
|
||||
blinker
|
||||
chardet
|
||||
docutils
|
||||
enum34
|
||||
flask
|
||||
flask-wtf
|
||||
flask-multistatic
|
||||
html5lib
|
||||
kitchen
|
||||
markdown
|
||||
munch
|
||||
Pillow
|
||||
psutil
|
||||
pygit2 >= 0.20.1
|
||||
pygments
|
||||
python-openid
|
||||
python-openid-cla
|
||||
python-openid-teams
|
||||
redis
|
||||
six
|
||||
sqlalchemy >= 0.8
|
||||
straight.plugin==1.4.0-post-1
|
||||
trollius-redis
|
||||
wtforms
|
||||
python-fedora
|
||||
cryptography
|
||||
py-bcrypt
|
1
pagure.egg-info/top_level.txt
Normal file
|
@ -0,0 +1 @@
|
|||
pagure
|
556
pagure/__init__.py
Normal file
|
@ -0,0 +1,556 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
(c) 2014-2015 - Copyright Red Hat Inc
|
||||
|
||||
Authors:
|
||||
Pierre-Yves Chibon <pingou@pingoured.fr>
|
||||
|
||||
"""
|
||||
|
||||
# These two lines are needed to run on EL6
|
||||
__requires__ = ['SQLAlchemy >= 0.8', 'jinja2 >= 2.4']
|
||||
import pkg_resources
|
||||
|
||||
__version__ = '2.3.4'
|
||||
__api_version__ = '0.7'
|
||||
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import urlparse
|
||||
from logging.handlers import SMTPHandler
|
||||
|
||||
import flask
|
||||
import pygit2
|
||||
import werkzeug
|
||||
from functools import wraps
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
|
||||
from pygments import highlight
|
||||
from pygments.lexers.text import DiffLexer
|
||||
from pygments.formatters import HtmlFormatter
|
||||
|
||||
from flask_multistatic import MultiStaticFlask
|
||||
|
||||
from werkzeug.routing import BaseConverter
|
||||
|
||||
import pagure.exceptions
|
||||
|
||||
# Create the application.
|
||||
APP = MultiStaticFlask(__name__)
|
||||
APP.jinja_env.trim_blocks = True
|
||||
APP.jinja_env.lstrip_blocks = True
|
||||
|
||||
# set up FAS
|
||||
APP.config.from_object('pagure.default_config')
|
||||
|
||||
if 'PAGURE_CONFIG' in os.environ:
|
||||
APP.config.from_envvar('PAGURE_CONFIG')
|
||||
|
||||
|
||||
if APP.config.get('THEME_TEMPLATE_FOLDER', False):
|
||||
# Jinja can be told to look for templates in different folders
|
||||
# That's what we do here
|
||||
template_folder = APP.config['THEME_TEMPLATE_FOLDER']
|
||||
if template_folder[0] != '/':
|
||||
template_folder= os.path.join(
|
||||
APP.root_path, APP.template_folder, template_folder)
|
||||
import jinja2
|
||||
# Jinja looks for the template in the order of the folders specified
|
||||
templ_loaders = [
|
||||
jinja2.FileSystemLoader(template_folder),
|
||||
APP.jinja_loader,
|
||||
]
|
||||
APP.jinja_loader = jinja2.ChoiceLoader(templ_loaders)
|
||||
|
||||
|
||||
if APP.config.get('THEME_STATIC_FOLDER', False):
|
||||
static_folder = APP.config['THEME_STATIC_FOLDER']
|
||||
if static_folder[0] != '/':
|
||||
static_folder= os.path.join(
|
||||
APP.root_path, 'static', static_folder)
|
||||
# Unlike templates, to serve static files from multiples folders we
|
||||
# need flask-multistatic
|
||||
APP.static_folder = [
|
||||
static_folder,
|
||||
os.path.join(APP.root_path, 'static'),
|
||||
]
|
||||
|
||||
|
||||
class RepoConverter(BaseConverter):
|
||||
|
||||
"""Like the default :class:`UnicodeConverter`, but it allows matching
|
||||
a single slash.
|
||||
:param map: the :class:`Map`.
|
||||
"""
|
||||
regex = '[^/]*(/[^/]+)?'
|
||||
#weight = 200
|
||||
|
||||
|
||||
APP.url_map.converters['repo'] = RepoConverter
|
||||
|
||||
import pagure.doc_utils
|
||||
import pagure.forms
|
||||
import pagure.lib
|
||||
import pagure.lib.git
|
||||
import pagure.login_forms
|
||||
import pagure.mail_logging
|
||||
import pagure.proxy
|
||||
|
||||
# Only import flask_fas_openid if it is needed
|
||||
if APP.config.get('PAGURE_AUTH', None) in ['fas', 'openid']:
|
||||
from flask_fas_openid import FAS
|
||||
FAS = FAS(APP)
|
||||
|
||||
@FAS.postlogin
|
||||
def set_user(return_url):
|
||||
''' After login method. '''
|
||||
try:
|
||||
pagure.lib.set_up_user(
|
||||
session=SESSION,
|
||||
username=flask.g.fas_user.username,
|
||||
fullname=flask.g.fas_user.fullname,
|
||||
default_email=flask.g.fas_user.email,
|
||||
ssh_key=flask.g.fas_user.get('ssh_key'),
|
||||
keydir=APP.config.get('GITOLITE_KEYDIR', None),
|
||||
)
|
||||
|
||||
# If groups are managed outside pagure, set up the user at login
|
||||
if not APP.config.get('ENABLE_GROUP_MNGT', False):
|
||||
user = pagure.lib.search_user(
|
||||
SESSION, username=flask.g.fas_user.username)
|
||||
groups = set(user.groups)
|
||||
fas_groups = set(flask.g.fas_user.groups)
|
||||
# Add the new groups
|
||||
for group in fas_groups - groups:
|
||||
group = pagure.lib.search_groups(
|
||||
SESSION, group_name=group)
|
||||
if not group:
|
||||
continue
|
||||
try:
|
||||
pagure.lib.add_user_to_group(
|
||||
session=SESSION,
|
||||
username=flask.g.fas_user.username,
|
||||
group=group,
|
||||
user=flask.g.fas_user.username,
|
||||
is_admin=is_admin(),
|
||||
)
|
||||
except pagure.exceptions.PagureException as err:
|
||||
LOG.debug(err)
|
||||
# Remove the old groups
|
||||
for group in groups - fas_groups:
|
||||
try:
|
||||
pagure.lib.delete_user_of_group(
|
||||
session=SESSION,
|
||||
username=flask.g.fas_user.username,
|
||||
groupname=group,
|
||||
user=flask.g.fas_user.username,
|
||||
is_admin=is_admin(),
|
||||
force=True,
|
||||
)
|
||||
except pagure.exceptions.PagureException as err:
|
||||
LOG.debug(err)
|
||||
|
||||
SESSION.commit()
|
||||
except SQLAlchemyError as err:
|
||||
SESSION.rollback()
|
||||
LOG.debug(err)
|
||||
LOG.exception(err)
|
||||
flask.flash(
|
||||
'Could not set up you as a user properly, please contact '
|
||||
'an admin', 'error')
|
||||
return flask.redirect(return_url)
|
||||
|
||||
|
||||
SESSION = pagure.lib.create_session(APP.config['DB_URL'])
|
||||
REDIS = None
|
||||
if APP.config['EVENTSOURCE_SOURCE'] or APP.config['WEBHOOK']:
|
||||
pagure.lib.set_redis(
|
||||
host=APP.config['REDIS_HOST'],
|
||||
port=APP.config['REDIS_PORT'],
|
||||
db=APP.config['REDIS_DB']
|
||||
)
|
||||
|
||||
if not APP.debug:
|
||||
APP.logger.addHandler(pagure.mail_logging.get_mail_handler(
|
||||
smtp_server=APP.config.get('SMTP_SERVER', '127.0.0.1'),
|
||||
mail_admin=APP.config.get('MAIL_ADMIN', APP.config['EMAIL_ERROR'])
|
||||
))
|
||||
|
||||
# Send classic logs into syslog
|
||||
SHANDLER = logging.StreamHandler()
|
||||
SHANDLER.setLevel(APP.config.get('LOG_LEVEL', 'INFO'))
|
||||
APP.logger.addHandler(SHANDLER)
|
||||
|
||||
LOG = APP.logger
|
||||
LOG.setLevel(APP.config.get('LOG_LEVEL', 'INFO'))
|
||||
|
||||
APP.wsgi_app = pagure.proxy.ReverseProxied(APP.wsgi_app)
|
||||
|
||||
|
||||
def authenticated():
|
||||
''' Utility function checking if the current user is logged in or not.
|
||||
'''
|
||||
return hasattr(flask.g, 'fas_user') and flask.g.fas_user is not None
|
||||
|
||||
|
||||
def logout():
|
||||
auth = APP.config.get('PAGURE_AUTH', None)
|
||||
if auth in ['fas', 'openid']:
|
||||
if hasattr(flask.g, 'fas_user') and flask.g.fas_user is not None:
|
||||
FAS.logout()
|
||||
elif auth == 'local':
|
||||
import pagure.ui.login as login
|
||||
login.logout()
|
||||
|
||||
|
||||
def api_authenticated():
|
||||
''' Utility function checking if the current user is logged in or not
|
||||
in the API.
|
||||
'''
|
||||
return hasattr(flask.g, 'fas_user') \
|
||||
and flask.g.fas_user is not None \
|
||||
and hasattr(flask.g, 'token') \
|
||||
and flask.g.token is not None
|
||||
|
||||
|
||||
def admin_session_timedout():
|
||||
''' Check if the current user has been authenticated for more than what
|
||||
is allowed (defaults to 15 minutes).
|
||||
If it is the case, the user is logged out and the method returns True,
|
||||
otherwise it returns False.
|
||||
'''
|
||||
timedout = False
|
||||
if not authenticated():
|
||||
return True
|
||||
login_time = flask.g.fas_user.login_time
|
||||
# This is because flask_fas_openid will store this as a posix timestamp
|
||||
if not isinstance(login_time, datetime.datetime):
|
||||
login_time = datetime.datetime.utcfromtimestamp(login_time)
|
||||
if (datetime.datetime.utcnow() - login_time) > \
|
||||
APP.config.get('ADMIN_SESSION_LIFETIME',
|
||||
datetime.timedelta(minutes=15)):
|
||||
timedout = True
|
||||
logout()
|
||||
return timedout
|
||||
|
||||
|
||||
def is_safe_url(target): # pragma: no cover
|
||||
""" Checks that the target url is safe and sending to the current
|
||||
website not some other malicious one.
|
||||
"""
|
||||
ref_url = urlparse.urlparse(flask.request.host_url)
|
||||
test_url = urlparse.urlparse(
|
||||
urlparse.urljoin(flask.request.host_url, target))
|
||||
return test_url.scheme in ('http', 'https') and \
|
||||
ref_url.netloc == test_url.netloc
|
||||
|
||||
|
||||
def is_admin():
|
||||
""" Return whether the user is admin for this application or not. """
|
||||
if not authenticated():
|
||||
return False
|
||||
|
||||
user = flask.g.fas_user
|
||||
|
||||
auth_method = APP.config.get('PAGURE_AUTH', None)
|
||||
if auth_method == 'fas':
|
||||
if not user.cla_done or len(user.groups) < 1:
|
||||
return False
|
||||
|
||||
admin_users = APP.config.get('PAGURE_ADMIN_USERS', [])
|
||||
if not isinstance(admin_users, list):
|
||||
admin_users = [admin_users]
|
||||
if user.username in admin_users:
|
||||
return True
|
||||
|
||||
admins = APP.config['ADMIN_GROUP']
|
||||
if isinstance(admins, basestring):
|
||||
admins = [admins]
|
||||
admins = set(admins)
|
||||
groups = set(flask.g.fas_user.groups)
|
||||
|
||||
return not groups.isdisjoint(admins)
|
||||
|
||||
|
||||
def is_repo_admin(repo_obj):
|
||||
""" Return whether the user is an admin of the provided repo. """
|
||||
if not authenticated():
|
||||
return False
|
||||
|
||||
user = flask.g.fas_user.username
|
||||
|
||||
admin_users = APP.config.get('PAGURE_ADMIN_USERS', [])
|
||||
if not isinstance(admin_users, list):
|
||||
admin_users = [admin_users]
|
||||
if user in admin_users:
|
||||
return True
|
||||
|
||||
usergrps = [
|
||||
usr.user
|
||||
for grp in repo_obj.groups
|
||||
for usr in grp.users]
|
||||
|
||||
return user == repo_obj.user.user or (
|
||||
user in [usr.user for usr in repo_obj.users]
|
||||
) or (user in usergrps)
|
||||
|
||||
|
||||
def generate_user_key_files():
|
||||
""" Regenerate the key files used by gitolite.
|
||||
"""
|
||||
gitolite_home = APP.config.get('GITOLITE_HOME', None)
|
||||
if gitolite_home:
|
||||
users = pagure.lib.search_user(SESSION)
|
||||
for user in users:
|
||||
pagure.lib.update_user_ssh(SESSION, user, user.public_ssh_key,
|
||||
APP.config.get('GITOLITE_KEYDIR', None))
|
||||
pagure.lib.git.generate_gitolite_acls()
|
||||
|
||||
|
||||
def login_required(function):
|
||||
""" Flask decorator to retrict access to logged in user.
|
||||
If the auth system is ``fas`` it will also require that the user sign
|
||||
the FPCA.
|
||||
"""
|
||||
auth_method = APP.config.get('PAGURE_AUTH', None)
|
||||
|
||||
@wraps(function)
|
||||
def decorated_function(*args, **kwargs):
|
||||
""" Decorated function, actually does the work. """
|
||||
if flask.session.get('_justloggedout', False):
|
||||
return flask.redirect(flask.url_for('.index'))
|
||||
elif not authenticated():
|
||||
return flask.redirect(
|
||||
flask.url_for('auth_login', next=flask.request.url))
|
||||
elif auth_method == 'fas' and not flask.g.fas_user.cla_done:
|
||||
flask.flash('You must sign the FPCA (Fedora Project Contributor '
|
||||
'Agreement) to use pagure', 'errors')
|
||||
return flask.redirect(flask.url_for('.index'))
|
||||
return function(*args, **kwargs)
|
||||
return decorated_function
|
||||
|
||||
|
||||
@APP.context_processor
|
||||
def inject_variables():
|
||||
""" With this decorator we can set some variables to all templates.
|
||||
"""
|
||||
user_admin = is_admin()
|
||||
|
||||
forkbuttonform = None
|
||||
if authenticated():
|
||||
forkbuttonform = pagure.forms.ConfirmationForm()
|
||||
|
||||
justlogedout = flask.session.get('_justloggedout', False)
|
||||
if justlogedout:
|
||||
flask.session['_justloggedout'] = None
|
||||
|
||||
def is_watching(reponame, username=None):
|
||||
watch = False
|
||||
if authenticated():
|
||||
watch = pagure.lib.is_watching(
|
||||
SESSION, flask.g.fas_user, reponame, repouser=username)
|
||||
return watch
|
||||
|
||||
return dict(
|
||||
version=__version__,
|
||||
admin=user_admin,
|
||||
authenticated=authenticated(),
|
||||
forkbuttonform=forkbuttonform,
|
||||
is_watching=is_watching,
|
||||
)
|
||||
|
||||
|
||||
# pylint: disable=W0613
|
||||
@APP.before_request
|
||||
def set_session():
|
||||
""" Set the flask session as permanent. """
|
||||
flask.session.permanent = True
|
||||
|
||||
|
||||
@APP.errorhandler(404)
|
||||
def not_found(error):
|
||||
"""404 Not Found page"""
|
||||
return flask.render_template('not_found.html', error=error), 404
|
||||
|
||||
|
||||
@APP.errorhandler(500)
|
||||
def fatal_error(error): # pragma: no cover
|
||||
"""500 Fatal Error page"""
|
||||
return flask.render_template('fatal_error.html', error=error), 500
|
||||
|
||||
|
||||
@APP.errorhandler(401)
|
||||
def unauthorized(error): # pragma: no cover
|
||||
"""401 Unauthorized page"""
|
||||
return flask.render_template('unauthorized.html', error=error), 401
|
||||
|
||||
|
||||
@APP.route('/login/', methods=('GET', 'POST'))
|
||||
def auth_login(): # pragma: no cover
|
||||
""" Method to log into the application using FAS OpenID. """
|
||||
return_point = flask.url_for('index')
|
||||
if 'next' in flask.request.args:
|
||||
if is_safe_url(flask.request.args['next']):
|
||||
return_point = flask.request.args['next']
|
||||
|
||||
if authenticated():
|
||||
return flask.redirect(return_point)
|
||||
|
||||
admins = APP.config['ADMIN_GROUP']
|
||||
if isinstance(admins, list):
|
||||
admins = set(admins)
|
||||
else: # pragma: no cover
|
||||
admins = set([admins])
|
||||
|
||||
if APP.config.get('PAGURE_AUTH', None) in ['fas', 'openid']:
|
||||
groups = set()
|
||||
if not APP.config.get('ENABLE_GROUP_MNGT', False):
|
||||
groups = [
|
||||
group.group_name
|
||||
for group in pagure.lib.search_groups(SESSION, group_type='user')
|
||||
]
|
||||
groups = set(groups).union(admins)
|
||||
return FAS.login(return_url=return_point, groups=groups)
|
||||
elif APP.config.get('PAGURE_AUTH', None) == 'local':
|
||||
form = pagure.login_forms.LoginForm()
|
||||
return flask.render_template(
|
||||
'login/login.html',
|
||||
next_url=return_point,
|
||||
form=form,
|
||||
)
|
||||
|
||||
|
||||
@APP.route('/logout/')
|
||||
def auth_logout(): # pragma: no cover
|
||||
""" Method to log out from the application. """
|
||||
return_point = flask.url_for('index')
|
||||
if 'next' in flask.request.args:
|
||||
if is_safe_url(flask.request.args['next']):
|
||||
return_point = flask.request.args['next']
|
||||
|
||||
if not authenticated():
|
||||
return flask.redirect(return_point)
|
||||
|
||||
logout()
|
||||
flask.flash("You have been logged out")
|
||||
flask.session['_justloggedout'] = True
|
||||
return flask.redirect(return_point)
|
||||
|
||||
|
||||
def __get_file_in_tree(repo_obj, tree, filepath, bail_on_tree=False):
|
||||
''' Retrieve the entry corresponding to the provided filename in a
|
||||
given tree.
|
||||
'''
|
||||
|
||||
filename = filepath[0]
|
||||
if isinstance(tree, pygit2.Blob):
|
||||
return
|
||||
for entry in tree:
|
||||
fname = entry.name.decode('utf-8')
|
||||
if fname == filename:
|
||||
if len(filepath) == 1:
|
||||
blob = repo_obj.get(entry.id)
|
||||
# If we can't get the content (for example: an empty folder)
|
||||
if blob is None:
|
||||
return
|
||||
# If we get a tree instead of a blob, let's escape
|
||||
if isinstance(blob, pygit2.Tree) and bail_on_tree:
|
||||
return blob
|
||||
content = blob.data
|
||||
# If it's a (sane) symlink, we try a single-level dereference
|
||||
if entry.filemode == pygit2.GIT_FILEMODE_LINK \
|
||||
and os.path.normpath(content) == content \
|
||||
and not os.path.isabs(content):
|
||||
try:
|
||||
dereferenced = tree[content]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
if dereferenced.filemode == pygit2.GIT_FILEMODE_BLOB:
|
||||
blob = repo_obj[dereferenced.oid]
|
||||
|
||||
return blob
|
||||
else:
|
||||
nextitem = repo_obj[entry.oid]
|
||||
# If we can't get the content (for example: an empty folder)
|
||||
if nextitem is None:
|
||||
return
|
||||
return __get_file_in_tree(
|
||||
repo_obj, nextitem, filepath[1:],
|
||||
bail_on_tree=bail_on_tree)
|
||||
|
||||
|
||||
def get_repo_path(repo):
|
||||
""" Return the path of the git repository corresponding to the provided
|
||||
Repository object from the DB.
|
||||
"""
|
||||
repopath = os.path.join(APP.config['GIT_FOLDER'], repo.path)
|
||||
|
||||
if not os.path.exists(repopath):
|
||||
flask.abort(404, 'No git repo found')
|
||||
|
||||
return repopath
|
||||
|
||||
|
||||
def get_remote_repo_path(remote_git, branch_from, loop=False):
|
||||
""" Return the path of the remote git repository corresponding to the
|
||||
provided information.
|
||||
"""
|
||||
repopath = os.path.join(
|
||||
APP.config['REMOTE_GIT_FOLDER'],
|
||||
werkzeug.secure_filename('%s_%s' % (remote_git, branch_from))
|
||||
)
|
||||
|
||||
if not os.path.exists(repopath):
|
||||
try:
|
||||
pygit2.clone_repository(
|
||||
remote_git, repopath, checkout_branch=branch_from)
|
||||
except Exception as err:
|
||||
LOG.debug(err)
|
||||
LOG.exception(err)
|
||||
flask.abort(500, 'Could not clone the remote git repository')
|
||||
else:
|
||||
repo = pagure.lib.repo.PagureRepo(repopath)
|
||||
try:
|
||||
repo.pull(branch=branch_from, force=True)
|
||||
except pagure.exceptions.PagureException as err:
|
||||
LOG.debug(err)
|
||||
LOG.exception(err)
|
||||
flask.abort(500, err.message)
|
||||
|
||||
return repopath
|
||||
|
||||
|
||||
# Import the application
|
||||
import pagure.ui.app
|
||||
import pagure.ui.admin
|
||||
import pagure.ui.fork
|
||||
import pagure.ui.groups
|
||||
if APP.config.get('ENABLE_TICKETS', True):
|
||||
import pagure.ui.issues
|
||||
import pagure.ui.plugins
|
||||
import pagure.ui.repo
|
||||
|
||||
from pagure.api import API
|
||||
APP.register_blueprint(API)
|
||||
|
||||
import pagure.internal
|
||||
APP.register_blueprint(pagure.internal.PV)
|
||||
|
||||
|
||||
# Only import the login controller if the app is set up for local login
|
||||
if APP.config.get('PAGURE_AUTH', None) == 'local':
|
||||
import pagure.ui.login as login
|
||||
APP.before_request(login._check_session_cookie)
|
||||
APP.after_request(login._send_session_cookie)
|
||||
|
||||
|
||||
# pylint: disable=W0613
|
||||
@APP.teardown_request
|
||||
def shutdown_session(exception=None):
|
||||
""" Remove the DB session at the end of each request. """
|
||||
SESSION.remove()
|
496
pagure/api/__init__.py
Normal file
|
@ -0,0 +1,496 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
(c) 2015 - Copyright Red Hat Inc
|
||||
|
||||
Authors:
|
||||
Pierre-Yves Chibon <pingou@pingoured.fr>
|
||||
|
||||
API namespace version 0.
|
||||
|
||||
"""
|
||||
|
||||
import codecs
|
||||
import functools
|
||||
import os
|
||||
|
||||
import docutils
|
||||
import enum
|
||||
import flask
|
||||
import markupsafe
|
||||
|
||||
API = flask.Blueprint('api_ns', __name__, url_prefix='/api/0')
|
||||
|
||||
|
||||
import pagure
|
||||
import pagure.lib
|
||||
from pagure import __api_version__, APP, SESSION, authenticated
|
||||
from pagure.doc_utils import load_doc, modify_rst, modify_html
|
||||
from pagure.exceptions import APIError
|
||||
|
||||
|
||||
def preload_docs(endpoint):
|
||||
''' Utility to load an RST file and turn it into fancy HTML. '''
|
||||
|
||||
here = os.path.dirname(os.path.abspath(__file__))
|
||||
fname = os.path.join(here, '..', 'doc', endpoint + '.rst')
|
||||
with codecs.open(fname, 'r', 'utf-8') as f:
|
||||
rst = f.read()
|
||||
|
||||
rst = modify_rst(rst)
|
||||
api_docs = docutils.examples.html_body(rst)
|
||||
api_docs = modify_html(api_docs)
|
||||
api_docs = markupsafe.Markup(api_docs)
|
||||
return api_docs
|
||||
|
||||
|
||||
APIDOC = preload_docs('api')
|
||||
|
||||
|
||||
class APIERROR(enum.Enum):
|
||||
""" Clast listing as Enum all the possible error thrown by the API.
|
||||
"""
|
||||
ENOCODE = 'Variable message describing the issue'
|
||||
ENOPROJECT = 'Project not found'
|
||||
ENOPROJECTS = 'No projects found'
|
||||
ETRACKERDISABLED = 'Issue tracker disabled for this project'
|
||||
EDBERROR = 'An error occured at the database level and prevent the ' \
|
||||
'action from reaching completion'
|
||||
EINVALIDREQ = 'Invalid or incomplete input submited'
|
||||
EINVALIDTOK = 'Invalid or expired token. Please visit %s to get or '\
|
||||
'renew your API token.' % APP.config['APP_URL']
|
||||
ENOISSUE = 'Issue not found'
|
||||
EISSUENOTALLOWED = 'You are not allowed to view this issue'
|
||||
EPULLREQUESTSDISABLED = 'Pull-Request have been deactivated for this '\
|
||||
'project'
|
||||
ENOREQ = 'Pull-Request not found'
|
||||
ENOPRCLOSE = 'You are not allowed to merge/close pull-request for '\
|
||||
'this project'
|
||||
EPRSCORE = 'This request does not have the minimum review score '\
|
||||
'necessary to be merged'
|
||||
ENOTASSIGNEE = 'Only the assignee can merge this review'
|
||||
ENOTASSIGNED = 'This request must be assigned to be merged'
|
||||
ENOUSER = 'No such user found'
|
||||
ENOCOMMENT = 'Comment not found'
|
||||
ENEWPROJECTDISABLED = 'Creating project have been disabled for this '\
|
||||
'instance'
|
||||
|
||||
|
||||
def check_api_acls(acls, optional=False):
|
||||
''' Checks if the user provided an API token with its request and if
|
||||
this token allows the user to access the endpoint desired.
|
||||
'''
|
||||
flask.g.token = None
|
||||
flask.g.user = None
|
||||
token = None
|
||||
token_str = None
|
||||
|
||||
if authenticated():
|
||||
return
|
||||
|
||||
if 'Authorization' in flask.request.headers:
|
||||
authorization = flask.request.headers['Authorization']
|
||||
if 'token' in authorization:
|
||||
token_str = authorization.split('token', 1)[1].strip()
|
||||
|
||||
token_auth = False
|
||||
if token_str:
|
||||
token = pagure.lib.get_api_token(SESSION, token_str)
|
||||
if token and not token.expired:
|
||||
if acls and set(token.acls_list).intersection(set(acls)):
|
||||
token_auth = True
|
||||
flask.g.fas_user = token.user
|
||||
flask.g.token = token
|
||||
elif not acls and optional:
|
||||
token_auth = True
|
||||
flask.g.fas_user = token.user
|
||||
flask.g.token = token
|
||||
elif optional:
|
||||
return
|
||||
|
||||
if not token_auth:
|
||||
output = {
|
||||
'error_code': APIERROR.EINVALIDTOK.name,
|
||||
'error': APIERROR.EINVALIDTOK.value,
|
||||
}
|
||||
jsonout = flask.jsonify(output)
|
||||
jsonout.status_code = 401
|
||||
return jsonout
|
||||
|
||||
|
||||
def api_login_required(acls=None):
|
||||
''' Decorator used to indicate that authentication is required for some
|
||||
API endpoint.
|
||||
'''
|
||||
|
||||
def decorator(fn):
|
||||
''' The decorator of the function '''
|
||||
|
||||
@functools.wraps(fn)
|
||||
def decorated_function(*args, **kwargs):
|
||||
''' Actually does the job with the arguments provided. '''
|
||||
|
||||
response = check_api_acls(acls)
|
||||
if response:
|
||||
return response
|
||||
return fn(*args, **kwargs)
|
||||
|
||||
return decorated_function
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def api_login_optional(acls=None):
|
||||
''' Decorator used to indicate that authentication is optional for some
|
||||
API endpoint.
|
||||
'''
|
||||
|
||||
def decorator(fn):
|
||||
''' The decorator of the function '''
|
||||
|
||||
@functools.wraps(fn)
|
||||
def decorated_function(*args, **kwargs):
|
||||
''' Actually does the job with the arguments provided. '''
|
||||
|
||||
response = check_api_acls(acls, optional=True)
|
||||
if response:
|
||||
return response
|
||||
return fn(*args, **kwargs)
|
||||
|
||||
return decorated_function
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def api_method(function):
|
||||
''' Runs an API endpoint and catch all the APIException thrown. '''
|
||||
|
||||
@functools.wraps(function)
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
result = function(*args, **kwargs)
|
||||
except APIError as e:
|
||||
if e.error_code in [APIERROR.EDBERROR]:
|
||||
APP.logger.exception(e)
|
||||
|
||||
if e.error_code in [APIERROR.ENOCODE]:
|
||||
response = flask.jsonify(
|
||||
{
|
||||
'error': e.error,
|
||||
'error_code': e.error_code.name
|
||||
}
|
||||
)
|
||||
else:
|
||||
response = flask.jsonify(
|
||||
{
|
||||
'error': e.error_code.value,
|
||||
'error_code': e.error_code.name,
|
||||
}
|
||||
)
|
||||
response.status_code = e.status_code
|
||||
else:
|
||||
response = result
|
||||
|
||||
return response
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
if pagure.APP.config.get('ENABLE_TICKETS', True):
|
||||
from pagure.api import issue
|
||||
from pagure.api import fork
|
||||
from pagure.api import project
|
||||
from pagure.api import user
|
||||
|
||||
|
||||
@API.route('/version/')
|
||||
@API.route('/version')
|
||||
def api_version():
|
||||
'''
|
||||
API Version
|
||||
-----------
|
||||
Get the current API version.
|
||||
|
||||
::
|
||||
|
||||
GET /api/0/version
|
||||
|
||||
Sample response
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
"version": "1"
|
||||
}
|
||||
|
||||
'''
|
||||
return flask.jsonify({'version': __api_version__})
|
||||
|
||||
|
||||
@API.route('/users/')
|
||||
@API.route('/users')
|
||||
def api_users():
|
||||
'''
|
||||
List users
|
||||
-----------
|
||||
Retrieve users that have logged into the Paugre instance.
|
||||
This can then be used as input for autocompletion in some forms/fields.
|
||||
|
||||
::
|
||||
|
||||
GET /api/0/users
|
||||
|
||||
Parameters
|
||||
^^^^^^^^^^
|
||||
|
||||
+---------------+----------+---------------+------------------------------+
|
||||
| Key | Type | Optionality | Description |
|
||||
+===============+==========+===============+==============================+
|
||||
| ``pattern`` | string | Optional | | Filters the starting |
|
||||
| | | | letters of the usernames |
|
||||
+---------------+----------+---------------+------------------------------+
|
||||
|
||||
Sample response
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
"total_users": 2,
|
||||
"users": ["user1", "user2"]
|
||||
}
|
||||
|
||||
'''
|
||||
pattern = flask.request.args.get('pattern', None)
|
||||
if pattern is not None and not pattern.endswith('*'):
|
||||
pattern += '*'
|
||||
|
||||
users = pagure.lib.search_user(SESSION, pattern=pattern)
|
||||
|
||||
return flask.jsonify(
|
||||
{
|
||||
'total_users': len(users),
|
||||
'users': [user.username for user in users],
|
||||
'mention': [{
|
||||
'username': user.username,
|
||||
'name': user.fullname,
|
||||
'image': pagure.lib.avatar_url_from_openid(user.default_email,
|
||||
size=16)
|
||||
} for user in users]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@API.route('/<repo>/tags')
|
||||
@API.route('/<repo>/tags/')
|
||||
@API.route('/fork/<username>/<repo>/tags')
|
||||
@API.route('/fork/<username>/<repo>/tags/')
|
||||
def api_project_tags(repo, username=None):
|
||||
'''
|
||||
List all the tags of a project
|
||||
------------------------------
|
||||
List the tags made on the project's issues.
|
||||
|
||||
::
|
||||
|
||||
GET /api/0/<repo>/tags
|
||||
|
||||
::
|
||||
|
||||
GET /api/0/fork/<username>/<repo>/tags
|
||||
|
||||
Parameters
|
||||
^^^^^^^^^^
|
||||
|
||||
+---------------+----------+---------------+--------------------------+
|
||||
| Key | Type | Optionality | Description |
|
||||
+===============+==========+===============+==========================+
|
||||
| ``pattern`` | string | Optional | | Filters the starting |
|
||||
| | | | letters of the tags |
|
||||
+---------------+----------+---------------+--------------------------+
|
||||
|
||||
Sample response
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
"total_tags": 2,
|
||||
"tags": ["tag1", "tag2"]
|
||||
}
|
||||
|
||||
'''
|
||||
pattern = flask.request.args.get('pattern', None)
|
||||
if pattern is not None and not pattern.endswith('*'):
|
||||
pattern += '*'
|
||||
|
||||
project_obj = pagure.lib.get_project(SESSION, repo, username)
|
||||
if not project_obj:
|
||||
output = {'output': 'notok', 'error': 'Project not found'}
|
||||
jsonout = flask.jsonify(output)
|
||||
jsonout.status_code = 404
|
||||
return jsonout
|
||||
|
||||
tags = pagure.lib.get_tags_of_project(
|
||||
SESSION, project_obj, pattern=pattern)
|
||||
|
||||
return flask.jsonify(
|
||||
{
|
||||
'total_tags': len(tags),
|
||||
'tags': [tag.tag for tag in tags]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@API.route('/groups/')
|
||||
@API.route('/groups')
|
||||
def api_groups():
|
||||
'''
|
||||
List groups
|
||||
-----------
|
||||
Retrieve groups on this Pagure instance.
|
||||
This can then be used as input for autocompletion in some forms/fields.
|
||||
|
||||
::
|
||||
|
||||
GET /api/0/groups
|
||||
|
||||
Parameters
|
||||
^^^^^^^^^^
|
||||
|
||||
+---------------+----------+---------------+--------------------------+
|
||||
| Key | Type | Optionality | Description |
|
||||
+===============+==========+===============+==========================+
|
||||
| ``pattern`` | string | Optional | | Filters the starting |
|
||||
| | | | letters of the group |
|
||||
| | | | names |
|
||||
+---------------+----------+---------------+--------------------------+
|
||||
|
||||
Sample response
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
"total_groups": 2,
|
||||
"groups": ["group1", "group2"]
|
||||
}
|
||||
|
||||
'''
|
||||
pattern = flask.request.args.get('pattern', None)
|
||||
if pattern is not None and not pattern.endswith('*'):
|
||||
pattern += '*'
|
||||
|
||||
groups = pagure.lib.search_groups(SESSION, pattern=pattern)
|
||||
|
||||
return flask.jsonify(
|
||||
{
|
||||
'total_groups': len(groups),
|
||||
'groups': [group.group_name for group in groups]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@API.route('/error_codes/')
|
||||
@API.route('/error_codes')
|
||||
def api_error_codes():
|
||||
'''
|
||||
Error codes
|
||||
------------
|
||||
Get a dictionary (hash) of all error codes.
|
||||
|
||||
::
|
||||
|
||||
GET /api/0/error_codes
|
||||
|
||||
Sample response
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
ENOCODE: 'Variable message describing the issue',
|
||||
ENOPROJECT: 'Project not found',
|
||||
}
|
||||
|
||||
'''
|
||||
errors = {val.name: val.value for val in APIERROR.__members__.values()}
|
||||
|
||||
return flask.jsonify(errors)
|
||||
|
||||
|
||||
@API.route('/')
|
||||
def api():
|
||||
''' Display the api information page. '''
|
||||
api_git_tags_doc = load_doc(project.api_git_tags)
|
||||
api_projects_doc = load_doc(project.api_projects)
|
||||
|
||||
issues = []
|
||||
if pagure.APP.config.get('ENABLE_TICKETS', True):
|
||||
issues.append(load_doc(issue.api_new_issue))
|
||||
issues.append(load_doc(issue.api_view_issues))
|
||||
issues.append(load_doc(issue.api_view_issue))
|
||||
issues.append(load_doc(issue.api_view_issue_comment))
|
||||
issues.append(load_doc(issue.api_comment_issue))
|
||||
|
||||
api_pull_request_views_doc = load_doc(fork.api_pull_request_views)
|
||||
api_pull_request_view_doc = load_doc(fork.api_pull_request_view)
|
||||
api_pull_request_merge_doc = load_doc(fork.api_pull_request_merge)
|
||||
api_pull_request_close_doc = load_doc(fork.api_pull_request_close)
|
||||
api_pull_request_add_comment_doc = load_doc(
|
||||
fork.api_pull_request_add_comment)
|
||||
api_pull_request_add_flag_doc = load_doc(fork.api_pull_request_add_flag)
|
||||
|
||||
api_new_project_doc = load_doc(project.api_new_project)
|
||||
|
||||
api_version_doc = load_doc(api_version)
|
||||
api_users_doc = load_doc(api_users)
|
||||
api_view_user_doc = load_doc(user.api_view_user)
|
||||
if pagure.APP.config.get('ENABLE_TICKETS', True):
|
||||
api_project_tags_doc = load_doc(api_project_tags)
|
||||
api_groups_doc = load_doc(api_groups)
|
||||
api_error_codes_doc = load_doc(api_error_codes)
|
||||
|
||||
extras = [
|
||||
api_version_doc,
|
||||
api_error_codes_doc,
|
||||
]
|
||||
|
||||
if pagure.APP.config.get('ENABLE_TICKETS', True):
|
||||
extras.append(api_project_tags_doc)
|
||||
|
||||
return flask.render_template(
|
||||
'api.html',
|
||||
version=__api_version__.split('.'),
|
||||
api_doc=APIDOC,
|
||||
projects=[
|
||||
api_new_project_doc,
|
||||
api_git_tags_doc,
|
||||
api_projects_doc,
|
||||
],
|
||||
issues=issues,
|
||||
requests=[
|
||||
api_pull_request_views_doc,
|
||||
api_pull_request_view_doc,
|
||||
api_pull_request_merge_doc,
|
||||
api_pull_request_close_doc,
|
||||
api_pull_request_add_comment_doc,
|
||||
api_pull_request_add_flag_doc,
|
||||
],
|
||||
users=[
|
||||
api_users_doc,
|
||||
api_view_user_doc,
|
||||
api_groups_doc,
|
||||
],
|
||||
extras=extras,
|
||||
)
|
||||
|
||||
|
||||
@APP.route('/api/')
|
||||
@APP.route('/api')
|
||||
def api_redirect():
|
||||
''' Redirects the user to the API documentation page.
|
||||
|
||||
'''
|
||||
return flask.redirect(flask.url_for('api_ns.api'))
|
650
pagure/api/fork.py
Normal file
|
@ -0,0 +1,650 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
(c) 2015 - Copyright Red Hat Inc
|
||||
|
||||
Authors:
|
||||
Pierre-Yves Chibon <pingou@pingoured.fr>
|
||||
|
||||
"""
|
||||
|
||||
import flask
|
||||
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
|
||||
import pagure
|
||||
import pagure.exceptions
|
||||
import pagure.lib
|
||||
from pagure import APP, SESSION, is_repo_admin
|
||||
from pagure.api import API, api_method, api_login_required, APIERROR
|
||||
|
||||
|
||||
@API.route('/<repo>/pull-requests')
|
||||
@API.route('/fork/<username>/<repo>/pull-requests')
|
||||
@api_method
|
||||
def api_pull_request_views(repo, username=None):
|
||||
"""
|
||||
List project's Pull-Requests
|
||||
----------------------------
|
||||
Retrieve pull requests of a project.
|
||||
|
||||
::
|
||||
|
||||
GET /api/0/<repo>/pull-requests
|
||||
|
||||
::
|
||||
|
||||
GET /api/0/fork/<username>/<repo>/pull-requests
|
||||
|
||||
Parameters
|
||||
^^^^^^^^^^
|
||||
|
||||
+---------------+----------+--------------+----------------------------+
|
||||
| Key | Type | Optionality | Description |
|
||||
+===============+==========+==============+============================+
|
||||
| ``status`` | string | Optional | | Filter the status of |
|
||||
| | | | pull requests. Default: |
|
||||
| | | | ``True`` (opened pull |
|
||||
| | | | requests), can be ``0`` |
|
||||
| | | | or ``closed`` for closed |
|
||||
| | | | requests or ``Merged`` |
|
||||
| | | | for merged requests. |
|
||||
| | | | ``All`` returns closed, |
|
||||
| | | | merged and open requests.|
|
||||
+---------------+----------+--------------+----------------------------+
|
||||
| ``assignee`` | string | Optional | | Filter the assignee of |
|
||||
| | | | pull requests |
|
||||
+---------------+----------+--------------+----------------------------+
|
||||
| ``author`` | string | Optional | | Filter the author of |
|
||||
| | | | pull requests |
|
||||
+---------------+----------+--------------+----------------------------+
|
||||
|
||||
Sample response
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
"args": {
|
||||
"assignee": null,
|
||||
"author": null,
|
||||
"status": true
|
||||
},
|
||||
"total_requests": 1,
|
||||
"requests": [
|
||||
{
|
||||
"assignee": null,
|
||||
"branch": "master",
|
||||
"branch_from": "master",
|
||||
"closed_at": null,
|
||||
"closed_by": null,
|
||||
"comments": [],
|
||||
"commit_start": null,
|
||||
"commit_stop": null,
|
||||
"date_created": "1431414800",
|
||||
"id": 1,
|
||||
"project": {
|
||||
"date_created": "1431414800",
|
||||
"description": "test project #1",
|
||||
"id": 1,
|
||||
"name": "test",
|
||||
"parent": null,
|
||||
"user": {
|
||||
"fullname": "PY C",
|
||||
"name": "pingou"
|
||||
}
|
||||
},
|
||||
"repo_from": {
|
||||
"date_created": "1431414800",
|
||||
"description": "test project #1",
|
||||
"id": 1,
|
||||
"name": "test",
|
||||
"parent": null,
|
||||
"user": {
|
||||
"fullname": "PY C",
|
||||
"name": "pingou"
|
||||
}
|
||||
},
|
||||
"status": true,
|
||||
"title": "test pull-request",
|
||||
"uid": "1431414800",
|
||||
"updated_on": "1431414800",
|
||||
"user": {
|
||||
"fullname": "PY C",
|
||||
"name": "pingou"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
repo = pagure.lib.get_project(SESSION, repo, user=username)
|
||||
|
||||
if repo is None:
|
||||
raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)
|
||||
|
||||
if not repo.settings.get('pull_requests', True):
|
||||
raise pagure.exceptions.APIError(
|
||||
404, error_code=APIERROR.EPULLREQUESTSDISABLED)
|
||||
|
||||
status = flask.request.args.get('status', True)
|
||||
assignee = flask.request.args.get('assignee', None)
|
||||
author = flask.request.args.get('author', None)
|
||||
|
||||
requests = []
|
||||
if str(status).lower() in ['0', 'false', 'closed']:
|
||||
requests = pagure.lib.search_pull_requests(
|
||||
SESSION,
|
||||
project_id=repo.id,
|
||||
status=False,
|
||||
assignee=assignee,
|
||||
author=author)
|
||||
|
||||
elif str(status).lower() == 'all':
|
||||
requests = pagure.lib.search_pull_requests(
|
||||
SESSION,
|
||||
project_id=repo.id,
|
||||
status=None,
|
||||
assignee=assignee,
|
||||
author=author)
|
||||
|
||||
else:
|
||||
requests = pagure.lib.search_pull_requests(
|
||||
SESSION,
|
||||
project_id=repo.id,
|
||||
assignee=assignee,
|
||||
author=author,
|
||||
status=status)
|
||||
|
||||
jsonout = flask.jsonify({
|
||||
'total_requests': len(requests),
|
||||
'requests': [
|
||||
request.to_json(public=True, api=True)
|
||||
for request in requests],
|
||||
'args': {
|
||||
'status': status,
|
||||
'assignee': assignee,
|
||||
'author': author,
|
||||
}
|
||||
})
|
||||
return jsonout
|
||||
|
||||
|
||||
@API.route('/<repo>/pull-request/<int:requestid>')
|
||||
@API.route('/fork/<username>/<repo>/pull-request/<int:requestid>')
|
||||
@api_method
|
||||
def api_pull_request_view(repo, requestid, username=None):
|
||||
"""
|
||||
Pull-request information
|
||||
------------------------
|
||||
Retrieve information of a specific pull request.
|
||||
|
||||
::
|
||||
|
||||
GET /api/0/<repo>/pull-request/<request id>
|
||||
|
||||
::
|
||||
|
||||
GET /api/0/fork/<username>/<repo>/pull-request/<request id>
|
||||
|
||||
Sample response
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
"assignee": null,
|
||||
"branch": "master",
|
||||
"branch_from": "master",
|
||||
"closed_at": null,
|
||||
"closed_by": null,
|
||||
"comments": [],
|
||||
"commit_start": null,
|
||||
"commit_stop": null,
|
||||
"date_created": "1431414800",
|
||||
"id": 1,
|
||||
"project": {
|
||||
"date_created": "1431414800",
|
||||
"description": "test project #1",
|
||||
"id": 1,
|
||||
"name": "test",
|
||||
"parent": null,
|
||||
"user": {
|
||||
"fullname": "PY C",
|
||||
"name": "pingou"
|
||||
}
|
||||
},
|
||||
"repo_from": {
|
||||
"date_created": "1431414800",
|
||||
"description": "test project #1",
|
||||
"id": 1,
|
||||
"name": "test",
|
||||
"parent": null,
|
||||
"user": {
|
||||
"fullname": "PY C",
|
||||
"name": "pingou"
|
||||
}
|
||||
},
|
||||
"status": true,
|
||||
"title": "test pull-request",
|
||||
"uid": "1431414800",
|
||||
"updated_on": "1431414800",
|
||||
"user": {
|
||||
"fullname": "PY C",
|
||||
"name": "pingou"
|
||||
}
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
repo = pagure.lib.get_project(SESSION, repo, user=username)
|
||||
|
||||
if repo is None:
|
||||
raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)
|
||||
|
||||
if not repo.settings.get('pull_requests', True):
|
||||
raise pagure.exceptions.APIError(
|
||||
404, error_code=APIERROR.EPULLREQUESTSDISABLED)
|
||||
|
||||
request = pagure.lib.search_pull_requests(
|
||||
SESSION, project_id=repo.id, requestid=requestid)
|
||||
|
||||
if not request:
|
||||
raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOREQ)
|
||||
|
||||
jsonout = flask.jsonify(request.to_json(public=True, api=True))
|
||||
return jsonout
|
||||
|
||||
|
||||
@API.route('/<repo>/pull-request/<int:requestid>/merge', methods=['POST'])
|
||||
@API.route('/fork/<username>/<repo>/pull-request/<int:requestid>/merge',
|
||||
methods=['POST'])
|
||||
@api_login_required(acls=['pull_request_merge'])
|
||||
@api_method
|
||||
def api_pull_request_merge(repo, requestid, username=None):
|
||||
"""
|
||||
Merge a pull-request
|
||||
--------------------
|
||||
Instruct Paugre to merge a pull request.
|
||||
|
||||
::
|
||||
|
||||
POST /api/0/<repo>/pull-request/<request id>/merge
|
||||
|
||||
::
|
||||
|
||||
POST /api/0/fork/<username>/<repo>/pull-request/<request id>/merge
|
||||
|
||||
Sample response
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
"message": "Changes merged!"
|
||||
}
|
||||
|
||||
"""
|
||||
output = {}
|
||||
|
||||
repo = pagure.lib.get_project(SESSION, repo, user=username)
|
||||
|
||||
if repo is None:
|
||||
raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)
|
||||
|
||||
if not repo.settings.get('pull_requests', True):
|
||||
raise pagure.exceptions.APIError(
|
||||
404, error_code=APIERROR.EPULLREQUESTSDISABLED)
|
||||
|
||||
if repo != flask.g.token.project:
|
||||
raise pagure.exceptions.APIError(401, error_code=APIERROR.EINVALIDTOK)
|
||||
|
||||
request = pagure.lib.search_pull_requests(
|
||||
SESSION, project_id=repo.id, requestid=requestid)
|
||||
|
||||
if not request:
|
||||
raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOREQ)
|
||||
|
||||
if not is_repo_admin(repo):
|
||||
raise pagure.exceptions.APIError(403, error_code=APIERROR.ENOPRCLOSE)
|
||||
|
||||
if repo.settings.get('Only_assignee_can_merge_pull-request', False):
|
||||
if not request.assignee:
|
||||
raise pagure.exceptions.APIError(
|
||||
403, error_code=APIERROR.ENOTASSIGNED)
|
||||
|
||||
if request.assignee.username != flask.g.fas_user.username:
|
||||
raise pagure.exceptions.APIError(
|
||||
403, error_code=APIERROR.ENOTASSIGNEE)
|
||||
|
||||
threshold = repo.settings.get('Minimum_score_to_merge_pull-request', -1)
|
||||
if threshold > 0 and int(request.score) < int(threshold):
|
||||
raise pagure.exceptions.APIError(403, error_code=APIERROR.EPRSCORE)
|
||||
|
||||
try:
|
||||
message = pagure.lib.git.merge_pull_request(
|
||||
SESSION, request, flask.g.fas_user.username,
|
||||
APP.config['REQUESTS_FOLDER'])
|
||||
output['message'] = message
|
||||
except pagure.exceptions.PagureException as err:
|
||||
raise pagure.exceptions.APIError(
|
||||
400, error_code=APIERROR.ENOCODE, error=str(err))
|
||||
|
||||
jsonout = flask.jsonify(output)
|
||||
return jsonout
|
||||
|
||||
|
||||
@API.route('/<repo>/pull-request/<int:requestid>/close', methods=['POST'])
|
||||
@API.route('/fork/<username>/<repo>/pull-request/<int:requestid>/close',
|
||||
methods=['POST'])
|
||||
@api_login_required(acls=['pull_request_close'])
|
||||
@api_method
|
||||
def api_pull_request_close(repo, requestid, username=None):
|
||||
"""
|
||||
Close a pull-request
|
||||
--------------------
|
||||
Instruct Pagure to close a pull request.
|
||||
|
||||
::
|
||||
|
||||
POST /api/0/<repo>/pull-request/<request id>/close
|
||||
|
||||
::
|
||||
|
||||
POST /api/0/fork/<username>/<repo>/pull-request/<request id>/close
|
||||
|
||||
Sample response
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
"message": "Pull-request closed!"
|
||||
}
|
||||
|
||||
"""
|
||||
output = {}
|
||||
|
||||
repo = pagure.lib.get_project(SESSION, repo, user=username)
|
||||
|
||||
if repo is None:
|
||||
raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)
|
||||
|
||||
if not repo.settings.get('pull_requests', True):
|
||||
raise pagure.exceptions.APIError(
|
||||
404, error_code=APIERROR.EPULLREQUESTSDISABLED)
|
||||
|
||||
if repo != flask.g.token.project:
|
||||
raise pagure.exceptions.APIError(401, error_code=APIERROR.EINVALIDTOK)
|
||||
|
||||
request = pagure.lib.search_pull_requests(
|
||||
SESSION, project_id=repo.id, requestid=requestid)
|
||||
|
||||
if not request:
|
||||
raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOREQ)
|
||||
|
||||
if not is_repo_admin(repo):
|
||||
raise pagure.exceptions.APIError(403, error_code=APIERROR.ENOPRCLOSE)
|
||||
|
||||
try:
|
||||
pagure.lib.close_pull_request(
|
||||
SESSION, request, flask.g.fas_user.username,
|
||||
requestfolder=APP.config['REQUESTS_FOLDER'],
|
||||
merged=False)
|
||||
SESSION.commit()
|
||||
output['message'] = 'Pull-request closed!'
|
||||
except SQLAlchemyError as err: # pragma: no cover
|
||||
SESSION.rollback()
|
||||
APP.logger.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('/fork/<username>/<repo>/pull-request/<int:requestid>/comment',
|
||||
methods=['POST'])
|
||||
@api_login_required(acls=['pull_request_comment'])
|
||||
@api_method
|
||||
def api_pull_request_add_comment(repo, requestid, username=None):
|
||||
"""
|
||||
Comment on a pull-request
|
||||
-------------------------
|
||||
Add comment to a pull request.
|
||||
|
||||
::
|
||||
|
||||
POST /api/0/<repo>/pull-request/<request id>/comment
|
||||
|
||||
::
|
||||
|
||||
POST /api/0/fork/<username>/<repo>/pull-request/<request id>/comment
|
||||
|
||||
Input
|
||||
^^^^^
|
||||
|
||||
+---------------+---------+--------------+-----------------------------+
|
||||
| Key | Type | Optionality | Description |
|
||||
+===============+=========+==============+=============================+
|
||||
| ``comment`` | string | Mandatory | | The comment to add |
|
||||
| | | | to the pull request |
|
||||
+---------------+---------+--------------+-----------------------------+
|
||||
| ``commit`` | string | Optional | | The hash of the specific |
|
||||
| | | | commit you wish to |
|
||||
| | | | comment on |
|
||||
+---------------+---------+--------------+-----------------------------+
|
||||
| ``filename`` | string | Optional | | The filename of the |
|
||||
| | | | specific file you wish |
|
||||
| | | | to comment on |
|
||||
+---------------+---------+--------------+-----------------------------+
|
||||
| ``row`` | int | Optional | | Used in combination |
|
||||
| | | | with filename to comment |
|
||||
| | | | on a specific row |
|
||||
| | | | of a file |
|
||||
+---------------+---------+--------------+-----------------------------+
|
||||
| ``tree_id`` | string | Optional | | The identifier of the |
|
||||
| | | | git tree as it was when |
|
||||
| | | | the comment was added |
|
||||
+---------------+---------+--------------+-----------------------------+
|
||||
|
||||
Sample response
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
"message": "Comment added"
|
||||
}
|
||||
|
||||
"""
|
||||
repo = pagure.lib.get_project(SESSION, repo, user=username)
|
||||
output = {}
|
||||
|
||||
if repo is None:
|
||||
raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)
|
||||
|
||||
if not repo.settings.get('pull_requests', True):
|
||||
raise pagure.exceptions.APIError(
|
||||
404, error_code=APIERROR.EPULLREQUESTSDISABLED)
|
||||
|
||||
if repo.fullname != flask.g.token.project.fullname:
|
||||
raise pagure.exceptions.APIError(401, error_code=APIERROR.EINVALIDTOK)
|
||||
|
||||
request = pagure.lib.search_pull_requests(
|
||||
SESSION, project_id=repo.id, requestid=requestid)
|
||||
|
||||
if not request:
|
||||
raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOREQ)
|
||||
|
||||
form = pagure.forms.AddPullRequestCommentForm(csrf_enabled=False)
|
||||
if form.validate_on_submit():
|
||||
comment = form.comment.data
|
||||
commit = form.commit.data or None
|
||||
filename = form.filename.data or None
|
||||
tree_id = form.tree_id.data or None
|
||||
row = form.row.data or None
|
||||
try:
|
||||
# New comment
|
||||
message = pagure.lib.add_pull_request_comment(
|
||||
SESSION,
|
||||
request=request,
|
||||
commit=commit,
|
||||
tree_id=tree_id,
|
||||
filename=filename,
|
||||
row=row,
|
||||
comment=comment,
|
||||
user=flask.g.fas_user.username,
|
||||
requestfolder=APP.config['REQUESTS_FOLDER'],
|
||||
)
|
||||
SESSION.commit()
|
||||
output['message'] = message
|
||||
except pagure.exceptions.PagureException as err:
|
||||
raise pagure.exceptions.APIError(
|
||||
400, error_code=APIERROR.ENOCODE, error=str(err))
|
||||
except SQLAlchemyError as err: # pragma: no cover
|
||||
APP.logger.exception(err)
|
||||
SESSION.rollback()
|
||||
raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR)
|
||||
|
||||
else:
|
||||
raise pagure.exceptions.APIError(400, error_code=APIERROR.EINVALIDREQ)
|
||||
|
||||
jsonout = flask.jsonify(output)
|
||||
return jsonout
|
||||
|
||||
|
||||
@API.route('/<repo>/pull-request/<int:requestid>/flag',
|
||||
methods=['POST'])
|
||||
@API.route('/fork/<username>/<repo>/pull-request/<int:requestid>/flag',
|
||||
methods=['POST'])
|
||||
@api_login_required(acls=['pull_request_flag'])
|
||||
@api_method
|
||||
def api_pull_request_add_flag(repo, requestid, username=None):
|
||||
"""
|
||||
Flag a pull-request
|
||||
-------------------
|
||||
Add or edit flags on a pull-request.
|
||||
|
||||
::
|
||||
|
||||
POST /api/0/<repo>/pull-request/<request id>/flag
|
||||
|
||||
::
|
||||
|
||||
POST /api/0/fork/<username>/<repo>/pull-request/<request id>/flag
|
||||
|
||||
Input
|
||||
^^^^^
|
||||
|
||||
+---------------+---------+--------------+-----------------------------+
|
||||
| Key | Type | Optionality | Description |
|
||||
+===============+=========+==============+=============================+
|
||||
| ``username`` | string | Mandatory | | The name of the |
|
||||
| | | | application to be |
|
||||
| | | | presented to users |
|
||||
| | | | on the pull request page |
|
||||
+---------------+---------+--------------+-----------------------------+
|
||||
| ``percent`` | int | Mandatory | | A percentage of |
|
||||
| | | | completion compared to |
|
||||
| | | | the goal. The percentage |
|
||||
| | | | also determine the |
|
||||
| | | | background color of the |
|
||||
| | | | flag on the pull-request |
|
||||
| | | | page |
|
||||
+---------------+---------+--------------+-----------------------------+
|
||||
| ``comment`` | string | Mandatory | | A short message |
|
||||
| | | | summarizing the |
|
||||
| | | | presented results |
|
||||
+---------------+---------+--------------+-----------------------------+
|
||||
| ``url`` | string | Mandatory | | A URL to the result |
|
||||
| | | | of this flag |
|
||||
+---------------+---------+--------------+-----------------------------+
|
||||
| ``uid`` | string | Optional | | A unique identifier used |
|
||||
| | | | to identify a flag on a |
|
||||
| | | | pull-request. If the |
|
||||
| | | | provided UID matches an |
|
||||
| | | | existing one, then the |
|
||||
| | | | API call will update the |
|
||||
| | | | existing one rather than |
|
||||
| | | | create a new one. |
|
||||
| | | | Maximum Length: 32 |
|
||||
| | | | characters. Default: an |
|
||||
| | | | auto generated UID |
|
||||
+---------------+---------+--------------+-----------------------------+
|
||||
| ``commit`` | string | Optional | | The hash of the commit |
|
||||
| | | | you use |
|
||||
+---------------+---------+--------------+-----------------------------+
|
||||
|
||||
Sample response
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
"message": "Flag added"
|
||||
}
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
"message": "Flag updated"
|
||||
}
|
||||
|
||||
"""
|
||||
repo = pagure.lib.get_project(SESSION, repo, user=username)
|
||||
output = {}
|
||||
|
||||
if repo is None:
|
||||
raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)
|
||||
|
||||
if not repo.settings.get('pull_requests', True):
|
||||
raise pagure.exceptions.APIError(
|
||||
404, error_code=APIERROR.EPULLREQUESTSDISABLED)
|
||||
|
||||
if repo.fullname != flask.g.token.project.fullname:
|
||||
raise pagure.exceptions.APIError(401, error_code=APIERROR.EINVALIDTOK)
|
||||
|
||||
request = pagure.lib.search_pull_requests(
|
||||
SESSION, project_id=repo.id, requestid=requestid)
|
||||
|
||||
if not request:
|
||||
raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOREQ)
|
||||
|
||||
form = pagure.forms.AddPullRequestFlagForm(csrf_enabled=False)
|
||||
if form.validate_on_submit():
|
||||
username = form.username.data
|
||||
percent = form.percent.data
|
||||
comment = form.comment.data.strip()
|
||||
url = form.url.data.strip()
|
||||
uid = form.uid.data.strip() if form.uid.data else None
|
||||
try:
|
||||
# New Flag
|
||||
message = pagure.lib.add_pull_request_flag(
|
||||
SESSION,
|
||||
request=request,
|
||||
username=username,
|
||||
percent=percent,
|
||||
comment=comment,
|
||||
url=url,
|
||||
uid=uid,
|
||||
user=flask.g.fas_user.username,
|
||||
requestfolder=APP.config['REQUESTS_FOLDER'],
|
||||
)
|
||||
SESSION.commit()
|
||||
output['message'] = message
|
||||
except pagure.exceptions.PagureException as err:
|
||||
raise pagure.exceptions.APIError(
|
||||
400, error_code=APIERROR.ENOCODE, error=str(err))
|
||||
except SQLAlchemyError as err: # pragma: no cover
|
||||
APP.logger.exception(err)
|
||||
SESSION.rollback()
|
||||
raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR)
|
||||
|
||||
else:
|
||||
raise pagure.exceptions.APIError(400, error_code=APIERROR.EINVALIDREQ)
|
||||
|
||||
jsonout = flask.jsonify(output)
|
||||
return jsonout
|
729
pagure/api/issue.py
Normal file
|
@ -0,0 +1,729 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
(c) 2015 - Copyright Red Hat Inc
|
||||
|
||||
Authors:
|
||||
Pierre-Yves Chibon <pingou@pingoured.fr>
|
||||
|
||||
"""
|
||||
|
||||
import flask
|
||||
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
|
||||
import pagure
|
||||
import pagure.exceptions
|
||||
import pagure.lib
|
||||
from pagure import APP, SESSION, is_repo_admin, api_authenticated
|
||||
from pagure.api import (
|
||||
API, api_method, api_login_required, api_login_optional, APIERROR
|
||||
)
|
||||
|
||||
|
||||
@API.route('/<repo>/new_issue', methods=['POST'])
|
||||
@API.route('/fork/<username>/<repo>/new_issue', methods=['POST'])
|
||||
@api_login_required(acls=['issue_create'])
|
||||
@api_method
|
||||
def api_new_issue(repo, username=None):
|
||||
"""
|
||||
Create a new issue
|
||||
------------------
|
||||
Open a new issue on a project.
|
||||
|
||||
::
|
||||
|
||||
POST /api/0/<repo>/new_issue
|
||||
|
||||
::
|
||||
|
||||
POST /api/0/fork/<username>/<repo>/new_issue
|
||||
|
||||
Input
|
||||
^^^^^
|
||||
|
||||
+--------------+----------+--------------+-----------------------------+
|
||||
| Key | Type | Optionality | Description |
|
||||
+==============+==========+==============+=============================+
|
||||
| ``title`` | string | Mandatory | The title of the issue |
|
||||
+--------------+----------+--------------+-----------------------------+
|
||||
| ``content`` | string | Mandatory | | The description of the |
|
||||
| | | | issue |
|
||||
+--------------+----------+--------------+-----------------------------+
|
||||
| ``private`` | boolean | Optional | | Include this key if |
|
||||
| | | | you want a private issue |
|
||||
| | | | to be created |
|
||||
+--------------+----------+--------------+-----------------------------+
|
||||
|
||||
Sample response
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
"message": "Issue created"
|
||||
}
|
||||
|
||||
"""
|
||||
repo = pagure.lib.get_project(SESSION, repo, user=username)
|
||||
output = {}
|
||||
|
||||
if repo is None:
|
||||
raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)
|
||||
|
||||
if not repo.settings.get('issue_tracker', True):
|
||||
raise pagure.exceptions.APIError(
|
||||
404, error_code=APIERROR.ETRACKERDISABLED)
|
||||
|
||||
if repo != flask.g.token.project:
|
||||
raise pagure.exceptions.APIError(401, error_code=APIERROR.EINVALIDTOK)
|
||||
|
||||
form = pagure.forms.IssueFormSimplied(csrf_enabled=False)
|
||||
if form.validate_on_submit():
|
||||
title = form.title.data
|
||||
content = form.issue_content.data
|
||||
private = str(form.private.data).lower() in ['true', '1']
|
||||
|
||||
try:
|
||||
issue = pagure.lib.new_issue(
|
||||
SESSION,
|
||||
repo=repo,
|
||||
title=title,
|
||||
content=content,
|
||||
private=private,
|
||||
user=flask.g.fas_user.username,
|
||||
ticketfolder=APP.config['TICKETS_FOLDER'],
|
||||
)
|
||||
SESSION.flush()
|
||||
# If there is a file attached, attach it.
|
||||
filestream = flask.request.files.get('filestream')
|
||||
if filestream and '<!!image>' in issue.content:
|
||||
new_filename = pagure.lib.git.add_file_to_git(
|
||||
repo=repo,
|
||||
issue=issue,
|
||||
ticketfolder=APP.config['TICKETS_FOLDER'],
|
||||
user=flask.g.fas_user,
|
||||
filename=filestream.filename,
|
||||
filestream=filestream.stream,
|
||||
)
|
||||
# Replace the <!!image> tag in the comment with the link
|
||||
# to the actual image
|
||||
filelocation = flask.url_for(
|
||||
'view_issue_raw_file',
|
||||
repo=repo.name,
|
||||
username=username,
|
||||
filename=new_filename,
|
||||
)
|
||||
new_filename = new_filename.split('-', 1)[1]
|
||||
url = '[![%s](%s)](%s)' % (
|
||||
new_filename, filelocation, filelocation)
|
||||
issue.content = issue.content.replace('<!!image>', url)
|
||||
SESSION.add(issue)
|
||||
SESSION.flush()
|
||||
|
||||
SESSION.commit()
|
||||
output['message'] = 'Issue created'
|
||||
except SQLAlchemyError as err: # pragma: no cover
|
||||
SESSION.rollback()
|
||||
APP.logger.exception(err)
|
||||
raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR)
|
||||
|
||||
else:
|
||||
raise pagure.exceptions.APIError(400, error_code=APIERROR.EINVALIDREQ)
|
||||
|
||||
jsonout = flask.jsonify(output)
|
||||
return jsonout
|
||||
|
||||
|
||||
@API.route('/<repo>/issues')
|
||||
@API.route('/fork/<username>/<repo>/issues')
|
||||
@api_login_optional()
|
||||
@api_method
|
||||
def api_view_issues(repo, username=None):
|
||||
"""
|
||||
List project's issues
|
||||
---------------------
|
||||
List issues of a project.
|
||||
|
||||
::
|
||||
|
||||
GET /api/0/<repo>/issues
|
||||
|
||||
::
|
||||
|
||||
GET /api/0/fork/<username>/<repo>/issues
|
||||
|
||||
Parameters
|
||||
^^^^^^^^^^
|
||||
|
||||
+---------------+---------+--------------+---------------------------+
|
||||
| Key | Type | Optionality | Description |
|
||||
+===============+=========+==============+===========================+
|
||||
| ``status`` | string | Optional | | Filters the status of |
|
||||
| | | | issues. Fetches all the |
|
||||
| | | | issues if status is |
|
||||
| | | | ``all``. Default: |
|
||||
| | | | ``Open`` |
|
||||
+---------------+---------+--------------+---------------------------+
|
||||
| ``tags`` | string | Optional | | A list of tags you |
|
||||
| | | | wish to filter. If |
|
||||
| | | | you want to filter |
|
||||
| | | | for issues not having |
|
||||
| | | | a tag, add an |
|
||||
| | | | exclamation mark in |
|
||||
| | | | front of it |
|
||||
+---------------+---------+--------------+---------------------------+
|
||||
| ``assignee`` | string | Optional | | Filter the issues |
|
||||
| | | | by assignee |
|
||||
+---------------+---------+--------------+---------------------------+
|
||||
| ``author`` | string | Optional | | Filter the issues |
|
||||
| | | | by creator |
|
||||
+---------------+---------+--------------+---------------------------+
|
||||
|
||||
Sample response
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
"args": {
|
||||
"assignee": null,
|
||||
"author": null,
|
||||
"status": "Closed",
|
||||
"tags": [
|
||||
"0.1"
|
||||
]
|
||||
},
|
||||
"total_issues": 1,
|
||||
"issues": [
|
||||
{
|
||||
"assignee": null,
|
||||
"blocks": [],
|
||||
"comments": [],
|
||||
"content": "asd",
|
||||
"date_created": "1427442217",
|
||||
"depends": [],
|
||||
"id": 4,
|
||||
"private": false,
|
||||
"status": "Fixed",
|
||||
"tags": [
|
||||
"0.1"
|
||||
],
|
||||
"title": "bug",
|
||||
"user": {
|
||||
"fullname": "PY.C",
|
||||
"name": "pingou"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
repo = pagure.lib.get_project(SESSION, repo, user=username)
|
||||
|
||||
if repo is None:
|
||||
raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)
|
||||
|
||||
if not repo.settings.get('issue_tracker', True):
|
||||
raise pagure.exceptions.APIError(
|
||||
404, error_code=APIERROR.ETRACKERDISABLED)
|
||||
|
||||
status = flask.request.args.get('status', None)
|
||||
tags = flask.request.args.getlist('tags')
|
||||
tags = [tag.strip() for tag in tags if tag.strip()]
|
||||
assignee = flask.request.args.get('assignee', None)
|
||||
author = flask.request.args.get('author', None)
|
||||
|
||||
# Hide private tickets
|
||||
private = False
|
||||
# If user is authenticated, show him/her his/her private tickets
|
||||
if api_authenticated():
|
||||
if repo != flask.g.token.project:
|
||||
raise pagure.exceptions.APIError(
|
||||
401, error_code=APIERROR.EINVALIDTOK)
|
||||
private = flask.g.fas_user.username
|
||||
# If user is repo admin, show all tickets included the private ones
|
||||
if is_repo_admin(repo):
|
||||
private = None
|
||||
|
||||
if status is not None:
|
||||
params = {
|
||||
'session': SESSION,
|
||||
'repo': repo,
|
||||
'tags': tags,
|
||||
'assignee': assignee,
|
||||
'author': author,
|
||||
'private': private
|
||||
}
|
||||
if status.lower() == 'closed':
|
||||
params.update({'closed': True})
|
||||
elif status.lower() != 'all':
|
||||
params.update({'status': status})
|
||||
issues = pagure.lib.search_issues(**params)
|
||||
else:
|
||||
issues = pagure.lib.search_issues(
|
||||
SESSION, repo, status='Open', tags=tags, assignee=assignee,
|
||||
author=author, private=private)
|
||||
|
||||
jsonout = flask.jsonify({
|
||||
'total_issues': len(issues),
|
||||
'issues': [issue.to_json(public=True) for issue in issues],
|
||||
'args': {
|
||||
'status': status,
|
||||
'tags': tags,
|
||||
'assignee': assignee,
|
||||
'author': author,
|
||||
}
|
||||
})
|
||||
return jsonout
|
||||
|
||||
|
||||
@API.route('/<repo>/issue/<issueid>')
|
||||
@API.route('/fork/<username>/<repo>/issue/<issueid>')
|
||||
@api_login_optional()
|
||||
@api_method
|
||||
def api_view_issue(repo, issueid, username=None):
|
||||
"""
|
||||
Issue information
|
||||
-----------------
|
||||
Retrieve information of a specific issue.
|
||||
|
||||
::
|
||||
|
||||
GET /api/0/<repo>/issue/<issue id>
|
||||
|
||||
::
|
||||
|
||||
GET /api/0/fork/<username>/<repo>/issue/<issue id>
|
||||
|
||||
The identifier provided can be either the unique identifier or the
|
||||
regular identifier used in the UI (for example ``24`` in
|
||||
``/forks/user/test/issue/24``)
|
||||
|
||||
Sample response
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
"assignee": null,
|
||||
"blocks": [],
|
||||
"comments": [],
|
||||
"content": "This issue needs attention",
|
||||
"date_created": "1431414800",
|
||||
"depends": [],
|
||||
"id": 1,
|
||||
"private": false,
|
||||
"status": "Open",
|
||||
"tags": [],
|
||||
"title": "test issue",
|
||||
"user": {
|
||||
"fullname": "PY C",
|
||||
"name": "pingou"
|
||||
}
|
||||
}
|
||||
|
||||
"""
|
||||
comments = flask.request.args.get('comments', True)
|
||||
if str(comments).lower() in ['0', 'False']:
|
||||
comments = False
|
||||
|
||||
repo = pagure.lib.get_project(SESSION, repo, user=username)
|
||||
|
||||
if repo is None:
|
||||
raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)
|
||||
|
||||
if not repo.settings.get('issue_tracker', True):
|
||||
raise pagure.exceptions.APIError(
|
||||
404, error_code=APIERROR.ETRACKERDISABLED)
|
||||
|
||||
issue_id = issue_uid = None
|
||||
try:
|
||||
issue_id = int(issueid)
|
||||
except:
|
||||
issue_uid = issueid
|
||||
|
||||
issue = pagure.lib.search_issues(
|
||||
SESSION, repo, issueid=issue_id, issueuid=issue_uid)
|
||||
|
||||
if issue is None or issue.project != repo:
|
||||
raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOISSUE)
|
||||
|
||||
if api_authenticated():
|
||||
if repo != flask.g.token.project:
|
||||
raise pagure.exceptions.APIError(
|
||||
401, error_code=APIERROR.EINVALIDTOK)
|
||||
|
||||
if issue.private and not is_repo_admin(repo) \
|
||||
and (not api_authenticated() or
|
||||
not issue.user.user == flask.g.fas_user.username):
|
||||
raise pagure.exceptions.APIError(
|
||||
403, error_code=APIERROR.EISSUENOTALLOWED)
|
||||
|
||||
jsonout = flask.jsonify(
|
||||
issue.to_json(public=True, with_comments=comments))
|
||||
return jsonout
|
||||
|
||||
|
||||
@API.route('/<repo>/issue/<issueid>/comment/<int:commentid>')
|
||||
@API.route('/fork/<username>/<repo>/issue/<issueid>/comment/<int:commentid>')
|
||||
@api_login_optional()
|
||||
@api_method
|
||||
def api_view_issue_comment(repo, issueid, commentid, username=None):
|
||||
"""
|
||||
Comment of an issue
|
||||
--------------------
|
||||
Retrieve a specific comment of an issue.
|
||||
|
||||
::
|
||||
|
||||
GET /api/0/<repo>/issue/<issue id>/comment/<comment id>
|
||||
|
||||
::
|
||||
|
||||
GET /api/0/fork/<username>/<repo>/issue/<issue id>/comment/<comment id>
|
||||
|
||||
The identifier provided can be either the unique identifier or the
|
||||
regular identifier used in the UI (for example ``24`` in
|
||||
``/forks/user/test/issue/24``)
|
||||
|
||||
Sample response
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
"avatar_url": "https://seccdn.libravatar.org/avatar/...",
|
||||
"comment": "9",
|
||||
"comment_date": "2015-07-01 15:08",
|
||||
"date_created": "1435756127",
|
||||
"id": 464,
|
||||
"parent": null,
|
||||
"user": {
|
||||
"fullname": "P.-Y.C.",
|
||||
"name": "pingou"
|
||||
}
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
repo = pagure.lib.get_project(SESSION, repo, user=username)
|
||||
|
||||
if repo is None:
|
||||
raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)
|
||||
|
||||
if not repo.settings.get('issue_tracker', True):
|
||||
raise pagure.exceptions.APIError(
|
||||
404, error_code=APIERROR.ETRACKERDISABLED)
|
||||
|
||||
issue_id = issue_uid = None
|
||||
try:
|
||||
issue_id = int(issueid)
|
||||
except:
|
||||
issue_uid = issueid
|
||||
|
||||
issue = pagure.lib.search_issues(
|
||||
SESSION, repo, issueid=issue_id, issueuid=issue_uid)
|
||||
|
||||
if issue is None or issue.project != repo:
|
||||
raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOISSUE)
|
||||
|
||||
if api_authenticated():
|
||||
if repo != flask.g.token.project:
|
||||
raise pagure.exceptions.APIError(
|
||||
401, error_code=APIERROR.EINVALIDTOK)
|
||||
|
||||
if issue.private and not is_repo_admin(issue.project) \
|
||||
and (not api_authenticated() or
|
||||
not issue.user.user == flask.g.fas_user.username):
|
||||
raise pagure.exceptions.APIError(
|
||||
403, error_code=APIERROR.EISSUENOTALLOWED)
|
||||
|
||||
comment = pagure.lib.get_issue_comment(SESSION, issue.uid, commentid)
|
||||
if not comment:
|
||||
raise pagure.exceptions.APIError(
|
||||
404, error_code=APIERROR.ENOCOMMENT)
|
||||
|
||||
output = comment.to_json(public=True)
|
||||
output['avatar_url'] = pagure.lib.avatar_url_from_openid(
|
||||
comment.user.default_email, size=16)
|
||||
output['comment_date'] = comment.date_created.strftime(
|
||||
'%Y-%m-%d %H:%M:%S')
|
||||
jsonout = flask.jsonify(output)
|
||||
return jsonout
|
||||
|
||||
|
||||
@API.route('/<repo>/issue/<int:issueid>/status', methods=['POST'])
|
||||
@API.route('/fork/<username>/<repo>/issue/<int:issueid>/status', methods=['POST'])
|
||||
@api_login_required(acls=['issue_change_status'])
|
||||
@api_method
|
||||
def api_change_status_issue(repo, issueid, username=None):
|
||||
"""
|
||||
Change issue status
|
||||
-------------------
|
||||
Change the status of an issue.
|
||||
|
||||
::
|
||||
|
||||
POST /api/0/<repo>/issue/<issue id>/status
|
||||
|
||||
::
|
||||
|
||||
POST /api/0/fork/<username>/<repo>/issue/<issue id>/status
|
||||
|
||||
Input
|
||||
^^^^^
|
||||
|
||||
+-------------+---------+--------------+------------------------------+
|
||||
| Key | Type | Optionality | Description |
|
||||
+=============+=========+==============+==============================+
|
||||
| ``status`` | string | Mandatory | The new status of the issue |
|
||||
+-------------+---------+--------------+------------------------------+
|
||||
|
||||
Sample response
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
"message": "Successfully edited issue #1"
|
||||
}
|
||||
|
||||
"""
|
||||
repo = pagure.lib.get_project(SESSION, repo, user=username)
|
||||
output = {}
|
||||
|
||||
if repo is None:
|
||||
raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)
|
||||
|
||||
if not repo.settings.get('issue_tracker', True):
|
||||
raise pagure.exceptions.APIError(
|
||||
404, error_code=APIERROR.ETRACKERDISABLED)
|
||||
|
||||
if api_authenticated():
|
||||
if repo != flask.g.token.project:
|
||||
raise pagure.exceptions.APIError(
|
||||
401, error_code=APIERROR.EINVALIDTOK)
|
||||
|
||||
issue = pagure.lib.search_issues(SESSION, repo, issueid=issueid)
|
||||
|
||||
if issue is None or issue.project != repo:
|
||||
raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOISSUE)
|
||||
|
||||
if issue.private and not is_repo_admin(repo) \
|
||||
and (not api_authenticated() or
|
||||
not issue.user.user == flask.g.fas_user.username):
|
||||
raise pagure.exceptions.APIError(
|
||||
403, error_code=APIERROR.EISSUENOTALLOWED)
|
||||
|
||||
status = pagure.lib.get_issue_statuses(SESSION)
|
||||
form = pagure.forms.StatusForm(status=status, csrf_enabled=False)
|
||||
if form.validate_on_submit():
|
||||
new_status = form.status.data
|
||||
try:
|
||||
# Update status
|
||||
message = pagure.lib.edit_issue(
|
||||
SESSION,
|
||||
issue=issue,
|
||||
status=new_status,
|
||||
user=flask.g.fas_user.username,
|
||||
ticketfolder=APP.config['TICKETS_FOLDER'],
|
||||
)
|
||||
SESSION.commit()
|
||||
if message:
|
||||
output['message'] = message
|
||||
else:
|
||||
output['message'] = 'No changes'
|
||||
except pagure.exceptions.PagureException as err:
|
||||
raise pagure.exceptions.APIError(
|
||||
400, error_code=APIERROR.ENOCODE, error=str(err))
|
||||
except SQLAlchemyError as err: # pragma: no cover
|
||||
SESSION.rollback()
|
||||
raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR)
|
||||
|
||||
else:
|
||||
raise pagure.exceptions.APIError(400, error_code=APIERROR.EINVALIDREQ)
|
||||
|
||||
jsonout = flask.jsonify(output)
|
||||
return jsonout
|
||||
|
||||
|
||||
@API.route('/<repo>/issue/<int:issueid>/comment', methods=['POST'])
|
||||
@API.route('/fork/<username>/<repo>/issue/<int:issueid>/comment', methods=['POST'])
|
||||
@api_login_required(acls=['issue_comment'])
|
||||
@api_method
|
||||
def api_comment_issue(repo, issueid, username=None):
|
||||
"""
|
||||
Comment to an issue
|
||||
-------------------
|
||||
Add a comment to an issue.
|
||||
|
||||
::
|
||||
|
||||
POST /api/0/<repo>/issue/<issue id>/comment
|
||||
|
||||
::
|
||||
|
||||
POST /api/0/fork/<username>/<repo>/issue/<issue id>/comment
|
||||
|
||||
Input
|
||||
^^^^^
|
||||
|
||||
+--------------+----------+---------------+---------------------------+
|
||||
| Key | Type | Optionality | Description |
|
||||
+==============+==========+===============+===========================+
|
||||
| ``comment`` | string | Mandatory | | The comment to add to |
|
||||
| | | | the issue |
|
||||
+--------------+----------+---------------+---------------------------+
|
||||
|
||||
Sample response
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
"message": "Comment added"
|
||||
}
|
||||
|
||||
"""
|
||||
repo = pagure.lib.get_project(SESSION, repo, user=username)
|
||||
output = {}
|
||||
|
||||
if repo is None:
|
||||
raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)
|
||||
|
||||
if not repo.settings.get('issue_tracker', True):
|
||||
raise pagure.exceptions.APIError(
|
||||
404, error_code=APIERROR.ETRACKERDISABLED)
|
||||
|
||||
if api_authenticated():
|
||||
if repo != flask.g.token.project:
|
||||
raise pagure.exceptions.APIError(
|
||||
401, error_code=APIERROR.EINVALIDTOK)
|
||||
|
||||
issue = pagure.lib.search_issues(SESSION, repo, issueid=issueid)
|
||||
|
||||
if issue is None or issue.project != repo:
|
||||
raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOISSUE)
|
||||
|
||||
if issue.private and not is_repo_admin(repo) \
|
||||
and (not api_authenticated() or
|
||||
not issue.user.user == flask.g.fas_user.username):
|
||||
raise pagure.exceptions.APIError(
|
||||
403, error_code=APIERROR.EISSUENOTALLOWED)
|
||||
|
||||
form = pagure.forms.CommentForm(csrf_enabled=False)
|
||||
if form.validate_on_submit():
|
||||
comment = form.comment.data
|
||||
try:
|
||||
# New comment
|
||||
message = pagure.lib.add_issue_comment(
|
||||
SESSION,
|
||||
issue=issue,
|
||||
comment=comment,
|
||||
user=flask.g.fas_user.username,
|
||||
ticketfolder=APP.config['TICKETS_FOLDER'],
|
||||
)
|
||||
SESSION.commit()
|
||||
output['message'] = message
|
||||
except SQLAlchemyError as err: # pragma: no cover
|
||||
SESSION.rollback()
|
||||
APP.logger.exception(err)
|
||||
raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR)
|
||||
|
||||
else:
|
||||
raise pagure.exceptions.APIError(400, error_code=APIERROR.EINVALIDREQ)
|
||||
|
||||
jsonout = flask.jsonify(output)
|
||||
return jsonout
|
||||
|
||||
|
||||
@API.route('/<repo>/issue/<int:issueid>/assign', methods=['POST'])
|
||||
@API.route('/fork/<username>/<repo>/issue/<int:issueid>/assign', methods=['POST'])
|
||||
@api_login_required(acls=['issue_assign'])
|
||||
@api_method
|
||||
def api_assign_issue(repo, issueid, username=None):
|
||||
"""
|
||||
Assign an issue
|
||||
---------------
|
||||
Assign an issue to someone.
|
||||
|
||||
::
|
||||
|
||||
POST /api/0/<repo>/issue/<issue id>/assign
|
||||
|
||||
::
|
||||
|
||||
POST /api/0/fork/<username>/<repo>/issue/<issue id>/assign
|
||||
|
||||
Input
|
||||
^^^^^
|
||||
|
||||
+--------------+----------+---------------+---------------------------+
|
||||
| Key | Type | Optionality | Description |
|
||||
+==============+==========+===============+===========================+
|
||||
| ``assignee`` | string | Mandatory | | The username of the user|
|
||||
| | | | to assign the issue to. |
|
||||
+--------------+----------+---------------+---------------------------+
|
||||
|
||||
Sample response
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
"message": "Issue assigned"
|
||||
}
|
||||
|
||||
"""
|
||||
repo = pagure.lib.get_project(SESSION, repo, user=username)
|
||||
output = {}
|
||||
|
||||
if repo is None:
|
||||
raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)
|
||||
|
||||
if not repo.settings.get('issue_tracker', True):
|
||||
raise pagure.exceptions.APIError(
|
||||
404, error_code=APIERROR.ETRACKERDISABLED)
|
||||
|
||||
if api_authenticated():
|
||||
if repo != flask.g.token.project:
|
||||
raise pagure.exceptions.APIError(
|
||||
401, error_code=APIERROR.EINVALIDTOK)
|
||||
|
||||
issue = pagure.lib.search_issues(SESSION, repo, issueid=issueid)
|
||||
|
||||
if issue is None or issue.project != repo:
|
||||
raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOISSUE)
|
||||
|
||||
if issue.private and not is_repo_admin(repo) \
|
||||
and (not api_authenticated() or
|
||||
not issue.user.user == flask.g.fas_user.username):
|
||||
raise pagure.exceptions.APIError(
|
||||
403, error_code=APIERROR.EISSUENOTALLOWED)
|
||||
|
||||
form = pagure.forms.AssignIssueForm(csrf_enabled=False)
|
||||
if form.validate_on_submit():
|
||||
assignee = form.assignee.data
|
||||
try:
|
||||
# New comment
|
||||
message = pagure.lib.add_issue_assignee(
|
||||
SESSION,
|
||||
issue=issue,
|
||||
assignee=assignee,
|
||||
user=flask.g.fas_user.username,
|
||||
ticketfolder=APP.config['TICKETS_FOLDER'],
|
||||
)
|
||||
SESSION.commit()
|
||||
output['message'] = message
|
||||
except SQLAlchemyError as err: # pragma: no cover
|
||||
SESSION.rollback()
|
||||
APP.logger.exception(err)
|
||||
raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR)
|
||||
|
||||
else:
|
||||
raise pagure.exceptions.APIError(400, error_code=APIERROR.EINVALIDREQ)
|
||||
|
||||
jsonout = flask.jsonify(output)
|
||||
return jsonout
|
346
pagure/api/project.py
Normal file
|
@ -0,0 +1,346 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
(c) 2015 - Copyright Red Hat Inc
|
||||
|
||||
Authors:
|
||||
Pierre-Yves Chibon <pingou@pingoured.fr>
|
||||
|
||||
"""
|
||||
|
||||
import flask
|
||||
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
|
||||
import pagure
|
||||
import pagure.exceptions
|
||||
import pagure.lib
|
||||
from pagure import SESSION, APP
|
||||
from pagure.api import API, api_method, APIERROR, api_login_required
|
||||
|
||||
|
||||
@API.route('/<repo>/git/tags')
|
||||
@API.route('/fork/<username>/<repo>/git/tags')
|
||||
@api_method
|
||||
def api_git_tags(repo, username=None):
|
||||
"""
|
||||
Project git tags
|
||||
----------------
|
||||
List the tags made on the project Git repository.
|
||||
|
||||
::
|
||||
|
||||
GET /api/0/<repo>/git/tags
|
||||
|
||||
::
|
||||
|
||||
GET /api/0/fork/<username>/<repo>/git/tags
|
||||
|
||||
Sample response
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
"total_tags": 2,
|
||||
"tags": ["0.0.1", "0.0.2"]
|
||||
}
|
||||
|
||||
"""
|
||||
repo = pagure.lib.get_project(SESSION, repo, user=username)
|
||||
|
||||
if repo is None:
|
||||
raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)
|
||||
|
||||
tags = pagure.lib.git.get_git_tags(repo)
|
||||
|
||||
jsonout = flask.jsonify({
|
||||
'total_tags': len(tags),
|
||||
'tags': tags
|
||||
})
|
||||
return jsonout
|
||||
|
||||
|
||||
@API.route('/projects')
|
||||
@api_method
|
||||
def api_projects():
|
||||
"""
|
||||
List projects
|
||||
--------------
|
||||
Search projects given the specified criterias.
|
||||
|
||||
::
|
||||
|
||||
GET /api/0/projects
|
||||
|
||||
::
|
||||
|
||||
GET /api/0/projects?tags=fedora-infra
|
||||
|
||||
Parameters
|
||||
^^^^^^^^^^
|
||||
|
||||
+---------------+----------+---------------+--------------------------+
|
||||
| Key | Type | Optionality | Description |
|
||||
+===============+==========+===============+==========================+
|
||||
| ``tags`` | string | Optional | | Filters the projects |
|
||||
| | | | returned by their tags |
|
||||
+---------------+----------+---------------+--------------------------+
|
||||
| ``pattern`` | string | Optional | | Filters the projects |
|
||||
| | | | by the pattern string |
|
||||
+---------------+----------+---------------+--------------------------+
|
||||
| ``username`` | string | Optional | | Filters the projects |
|
||||
| | | | returned by the users |
|
||||
| | | | having commit rights |
|
||||
| | | | to it |
|
||||
+---------------+----------+---------------+--------------------------+
|
||||
| ``fork`` | boolean | Optional | | Filters the projects |
|
||||
| | | | returned depending if |
|
||||
| | | | they are forks or not |
|
||||
+---------------+----------+---------------+--------------------------+
|
||||
|
||||
Sample response
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
"total_projects": 2,
|
||||
"projects": [
|
||||
{
|
||||
"date_created": "1427441537",
|
||||
"description": "A web-based calendar for Fedora",
|
||||
"id": 7,
|
||||
"name": "fedocal",
|
||||
"parent": null,
|
||||
"user": {
|
||||
"fullname": "Pierre-Yves C",
|
||||
"name": "pingou"
|
||||
}
|
||||
},
|
||||
{
|
||||
"date_created": "1431666007",
|
||||
"description": "An awesome messaging servicefor everyone",
|
||||
"id": 12,
|
||||
"name": "fedmsg",
|
||||
"parent": {
|
||||
"date_created": "1433423298",
|
||||
"description": "An awesome messaging servicefor everyone",
|
||||
"id": 11,
|
||||
"name": "fedmsg",
|
||||
"parent": null,
|
||||
"user": {
|
||||
"fullname": "Ralph B",
|
||||
"name": "ralph"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"fullname": "Pierre-Yves C",
|
||||
"name": "pingou"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
"""
|
||||
tags = flask.request.values.getlist('tags')
|
||||
username = flask.request.values.get('username', None)
|
||||
fork = flask.request.values.get('fork', None)
|
||||
pattern = flask.request.values.get('pattern', None)
|
||||
|
||||
if str(fork).lower() in ['1', 'true']:
|
||||
fork = True
|
||||
elif str(fork).lower() in ['0', 'false']:
|
||||
fork = False
|
||||
|
||||
projects = pagure.lib.search_projects(
|
||||
SESSION, username=username, fork=fork, tags=tags, pattern=pattern)
|
||||
|
||||
if not projects:
|
||||
raise pagure.exceptions.APIError(
|
||||
404, error_code=APIERROR.ENOPROJECTS)
|
||||
|
||||
jsonout = flask.jsonify({
|
||||
'total_projects': len(projects),
|
||||
'projects': [p.to_json(api=True, public=True) for p in projects]
|
||||
})
|
||||
return jsonout
|
||||
|
||||
|
||||
@API.route('/new/', methods=['POST'])
|
||||
@API.route('/new', methods=['POST'])
|
||||
@api_login_required(acls=['create_project'])
|
||||
@api_method
|
||||
def api_new_project():
|
||||
"""
|
||||
Create a new project
|
||||
--------------------
|
||||
Create a new project on this pagure instance.
|
||||
|
||||
::
|
||||
|
||||
POST /api/0/<repo>/new
|
||||
|
||||
|
||||
Input
|
||||
^^^^^
|
||||
|
||||
+------------------+---------+--------------+---------------------------+
|
||||
| Key | Type | Optionality | Description |
|
||||
+==================+=========+==============+===========================+
|
||||
| ``name`` | string | Mandatory | | The name of the new |
|
||||
| | | | project. |
|
||||
+------------------+---------+--------------+---------------------------+
|
||||
| ``description`` | string | Mandatory | | A short description of |
|
||||
| | | | the new project. |
|
||||
+------------------+---------+--------------+---------------------------+
|
||||
| ``url`` | string | Optional | | An url providing more |
|
||||
| | | | information about the |
|
||||
| | | | project. |
|
||||
+------------------+---------+--------------+---------------------------+
|
||||
| ``avatar_email`` | string | Optional | | An email address for the|
|
||||
| | | | avatar of the project. |
|
||||
+------------------+---------+--------------+---------------------------+
|
||||
| ``create_readme``| boolean | Optional | | A boolean to specify if |
|
||||
| | | | there should be a readme|
|
||||
| | | | added to the project on |
|
||||
| | | | creation. |
|
||||
+------------------+---------+--------------+---------------------------+
|
||||
|
||||
Sample response
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
'message': 'Project "foo" created'
|
||||
}
|
||||
|
||||
"""
|
||||
user = pagure.lib.search_user(SESSION, username=flask.g.fas_user.username)
|
||||
output = {}
|
||||
|
||||
if not pagure.APP.config.get('ENABLE_NEW_PROJECTS', True):
|
||||
raise pagure.exceptions.APIError(
|
||||
404, error_code=APIERROR.ENEWPROJECTDISABLED)
|
||||
|
||||
form = pagure.forms.ProjectForm(csrf_enabled=False)
|
||||
if form.validate_on_submit():
|
||||
name = form.name.data
|
||||
description = form.description.data
|
||||
url = form.url.data
|
||||
avatar_email = form.avatar_email.data
|
||||
create_readme = form.create_readme.data
|
||||
|
||||
try:
|
||||
message = pagure.lib.new_project(
|
||||
SESSION,
|
||||
name=name,
|
||||
description=description,
|
||||
url=url,
|
||||
avatar_email=avatar_email,
|
||||
user=flask.g.fas_user.username,
|
||||
blacklist=APP.config['BLACKLISTED_PROJECTS'],
|
||||
allowed_prefix=APP.config['ALLOWED_PREFIX'],
|
||||
gitfolder=APP.config['GIT_FOLDER'],
|
||||
docfolder=APP.config['DOCS_FOLDER'],
|
||||
ticketfolder=APP.config['TICKETS_FOLDER'],
|
||||
requestfolder=APP.config['REQUESTS_FOLDER'],
|
||||
add_readme=create_readme,
|
||||
userobj=user,
|
||||
)
|
||||
SESSION.commit()
|
||||
pagure.lib.git.generate_gitolite_acls()
|
||||
output['message'] = message
|
||||
except pagure.exceptions.PagureException as err:
|
||||
raise pagure.exceptions.APIError(
|
||||
400, error_code=APIERROR.ENOCODE, error=str(err))
|
||||
except SQLAlchemyError as err: # pragma: no cover
|
||||
APP.logger.exception(err)
|
||||
SESSION.rollback()
|
||||
raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR)
|
||||
else:
|
||||
raise pagure.exceptions.APIError(400, error_code=APIERROR.EINVALIDREQ)
|
||||
|
||||
jsonout = flask.jsonify(output)
|
||||
return jsonout
|
||||
|
||||
|
||||
@API.route('/fork/', methods=['POST'])
|
||||
@API.route('/fork', methods=['POST'])
|
||||
@api_login_required(acls=['fork_project'])
|
||||
@api_method
|
||||
def api_fork_project():
|
||||
"""
|
||||
Fork a project
|
||||
--------------------
|
||||
Fork a project on this pagure instance.
|
||||
|
||||
::
|
||||
|
||||
POST /api/0/<repo>/fork
|
||||
|
||||
|
||||
Input
|
||||
^^^^^
|
||||
|
||||
+------------------+---------+--------------+---------------------------+
|
||||
| Key | Type | Optionality | Description |
|
||||
+==================+=========+==============+===========================+
|
||||
| ``repo`` | string | Mandatory | | The name of the project |
|
||||
| | | | to fork. |
|
||||
+------------------+---------+--------------+---------------------------+
|
||||
| ``username`` | string | Optional | | The username of the user|
|
||||
| | | | of the fork. |
|
||||
+------------------+---------+--------------+---------------------------+
|
||||
|
||||
|
||||
Sample response
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
"message": 'Repo "test" cloned to "pingou/test"'
|
||||
}
|
||||
|
||||
"""
|
||||
output = {}
|
||||
|
||||
form = pagure.forms.ForkRepoForm(csrf_enabled=False)
|
||||
if form.validate_on_submit():
|
||||
repo = form.repo.data
|
||||
username = form.username.data or None
|
||||
|
||||
repo = pagure.lib.get_project(SESSION, repo, user=username)
|
||||
if repo is None:
|
||||
raise pagure.exceptions.APIError(
|
||||
404, error_code=APIERROR.ENOPROJECT)
|
||||
|
||||
try:
|
||||
message = pagure.lib.fork_project(
|
||||
SESSION,
|
||||
user=flask.g.fas_user.username,
|
||||
repo=repo,
|
||||
gitfolder=APP.config['GIT_FOLDER'],
|
||||
docfolder=APP.config['DOCS_FOLDER'],
|
||||
ticketfolder=APP.config['TICKETS_FOLDER'],
|
||||
requestfolder=APP.config['REQUESTS_FOLDER'],
|
||||
)
|
||||
SESSION.commit()
|
||||
pagure.lib.git.generate_gitolite_acls()
|
||||
output['message'] = message
|
||||
except pagure.exceptions.PagureException as err:
|
||||
raise pagure.exceptions.APIError(
|
||||
400, error_code=APIERROR.ENOCODE, error=str(err))
|
||||
except SQLAlchemyError as err: # pragma: no cover
|
||||
APP.logger.exception(err)
|
||||
SESSION.rollback()
|
||||
raise pagure.exceptions.APIError(
|
||||
400, error_code=APIERROR.EDBERROR)
|
||||
else:
|
||||
raise pagure.exceptions.APIError(
|
||||
400, error_code=APIERROR.EINVALIDREQ)
|
||||
|
||||
jsonout = flask.jsonify(output)
|
||||
return jsonout
|
110
pagure/api/user.py
Normal file
|
@ -0,0 +1,110 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
(c) 2015 - Copyright Red Hat Inc
|
||||
|
||||
Authors:
|
||||
Pierre-Yves Chibon <pingou@pingoured.fr>
|
||||
|
||||
"""
|
||||
|
||||
import flask
|
||||
|
||||
import pagure
|
||||
import pagure.exceptions
|
||||
import pagure.lib
|
||||
from pagure import APP, SESSION
|
||||
from pagure.api import API, api_method, APIERROR
|
||||
|
||||
|
||||
@API.route('/user/<username>')
|
||||
@api_method
|
||||
def api_view_user(username):
|
||||
"""
|
||||
User information
|
||||
----------------
|
||||
Use this endpoint to retrieve information about a specific user.
|
||||
|
||||
::
|
||||
|
||||
GET /api/0/user/<username>
|
||||
|
||||
::
|
||||
|
||||
GET /api/0/user/ralph
|
||||
|
||||
Sample response
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
"forks": [],
|
||||
"repos": [
|
||||
{
|
||||
"date_created": "1426595173",
|
||||
"description": "",
|
||||
"id": 5,
|
||||
"name": "pagure",
|
||||
"parent": null,
|
||||
"settings": {
|
||||
"Minimum_score_to_merge_pull-request": -1,
|
||||
"Only_assignee_can_merge_pull-request": false,
|
||||
"Web-hooks": null,
|
||||
"issue_tracker": true,
|
||||
"project_documentation": true,
|
||||
"pull_requests": true
|
||||
},
|
||||
"user": {
|
||||
"fullname": "ralph",
|
||||
"name": "ralph"
|
||||
}
|
||||
}
|
||||
],
|
||||
"user": {
|
||||
"fullname": "ralph",
|
||||
"name": "ralph"
|
||||
}
|
||||
}
|
||||
|
||||
"""
|
||||
httpcode = 200
|
||||
output = {}
|
||||
|
||||
user = pagure.lib.search_user(SESSION, username=username)
|
||||
if not user:
|
||||
raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOUSER)
|
||||
|
||||
repopage = flask.request.args.get('repopage', 1)
|
||||
try:
|
||||
repopage = int(repopage)
|
||||
except ValueError:
|
||||
repopage = 1
|
||||
|
||||
forkpage = flask.request.args.get('forkpage', 1)
|
||||
try:
|
||||
forkpage = int(forkpage)
|
||||
except ValueError:
|
||||
forkpage = 1
|
||||
|
||||
limit = APP.config['ITEM_PER_PAGE']
|
||||
repo_start = limit * (repopage - 1)
|
||||
fork_start = limit * (forkpage - 1)
|
||||
|
||||
repos = pagure.lib.search_projects(
|
||||
SESSION,
|
||||
username=username,
|
||||
fork=False)
|
||||
|
||||
forks = pagure.lib.search_projects(
|
||||
SESSION,
|
||||
username=username,
|
||||
fork=True)
|
||||
|
||||
output['user'] = user.to_json(public=True)
|
||||
output['repos'] = [repo.to_json(public=True) for repo in repos]
|
||||
output['forks'] = [repo.to_json(public=True) for repo in forks]
|
||||
|
||||
jsonout = flask.jsonify(output)
|
||||
jsonout.status_code = httpcode
|
||||
return jsonout
|
219
pagure/default_config.py
Normal file
|
@ -0,0 +1,219 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
(c) 2014-2015 - Copyright Red Hat Inc
|
||||
|
||||
Authors:
|
||||
Pierre-Yves Chibon <pingou@pingoured.fr>
|
||||
|
||||
"""
|
||||
|
||||
import os
|
||||
from datetime import timedelta
|
||||
|
||||
|
||||
# Set the time after which the admin session expires
|
||||
ADMIN_SESSION_LIFETIME = timedelta(minutes=20)
|
||||
|
||||
# secret key used to generate unique csrf token
|
||||
SECRET_KEY = '<insert here your own key>'
|
||||
|
||||
# url to the database server:
|
||||
DB_URL = 'sqlite:////var/tmp/pagure_dev.sqlite'
|
||||
|
||||
# url to datagrepper (optional):
|
||||
#DATAGREPPER_URL = 'https://apps.fedoraproject.org/datagrepper'
|
||||
#DATAGREPPER_CATEGORY = 'pagure'
|
||||
|
||||
# The FAS group in which the admin of pagure are
|
||||
ADMIN_GROUP = 'sysadmin-main'
|
||||
|
||||
# Hard-code a list of users that are global admins
|
||||
PAGURE_ADMIN_USERS = []
|
||||
|
||||
# Whether or not to send emails
|
||||
EMAIL_SEND = False
|
||||
|
||||
# The email address to which the flask.log will send the errors (tracebacks)
|
||||
EMAIL_ERROR = 'pingou@pingoured.fr'
|
||||
|
||||
# The URL at which the project is available.
|
||||
APP_URL = 'https://pagure.org/'
|
||||
|
||||
|
||||
# Enables / Disables tickets for project for the entire pagure instance
|
||||
ENABLE_TICKETS = True
|
||||
|
||||
# Enables / Disables creating projects on this pagure instance
|
||||
ENABLE_NEW_PROJECTS = True
|
||||
|
||||
# Enables / Disables deleting projects on this pagure instance
|
||||
ENABLE_DEL_PROJECTS = True
|
||||
|
||||
# Enables / Disables managing access to the repos
|
||||
ENABLE_USER_MNGT = True
|
||||
|
||||
# Enables / Disables managing groups via the UI
|
||||
ENABLE_GROUP_MNGT = True
|
||||
|
||||
# Enables / Disables showing all the projects by default on the front page
|
||||
SHOW_PROJECTS_INDEX = ['repos', 'myrepos', 'myforks']
|
||||
|
||||
# The URL to use to clone the git repositories.
|
||||
GIT_URL_SSH = 'ssh://git@pagure.org/'
|
||||
GIT_URL_GIT = 'git://pagure.org/'
|
||||
|
||||
|
||||
# Number of items displayed per page
|
||||
ITEM_PER_PAGE = 48
|
||||
|
||||
# Maximum size of the uploaded content
|
||||
MAX_CONTENT_LENGTH = 4 * 1024 * 1024 # 4 megabytes
|
||||
|
||||
# IP addresses allowed to access the internal endpoints
|
||||
IP_ALLOWED_INTERNAL = ['127.0.0.1', 'localhost', '::1']
|
||||
|
||||
# Redis configuration
|
||||
EVENTSOURCE_SOURCE = None
|
||||
WEBHOOK = False
|
||||
REDIS_HOST = '0.0.0.0'
|
||||
REDIS_PORT = 6379
|
||||
REDIS_DB = 0
|
||||
EVENTSOURCE_PORT = 8080
|
||||
|
||||
# Folder containing to the git repos
|
||||
GIT_FOLDER = os.path.join(
|
||||
os.path.abspath(os.path.dirname(__file__)),
|
||||
'..',
|
||||
'repos'
|
||||
)
|
||||
|
||||
# Folder containing the forks repos
|
||||
FORK_FOLDER = os.path.join(
|
||||
os.path.abspath(os.path.dirname(__file__)),
|
||||
'..',
|
||||
'forks'
|
||||
)
|
||||
|
||||
# Folder containing the docs repos
|
||||
DOCS_FOLDER = os.path.join(
|
||||
os.path.abspath(os.path.dirname(__file__)),
|
||||
'..',
|
||||
'docs'
|
||||
)
|
||||
|
||||
# Folder containing the tickets repos
|
||||
TICKETS_FOLDER = os.path.join(
|
||||
os.path.abspath(os.path.dirname(__file__)),
|
||||
'..',
|
||||
'tickets'
|
||||
)
|
||||
|
||||
# Folder containing the pull-requests repos
|
||||
REQUESTS_FOLDER = os.path.join(
|
||||
os.path.abspath(os.path.dirname(__file__)),
|
||||
'..',
|
||||
'requests'
|
||||
)
|
||||
|
||||
# Folder containing the clones for the remote pull-requests
|
||||
REMOTE_GIT_FOLDER = os.path.join(
|
||||
os.path.abspath(os.path.dirname(__file__)),
|
||||
'..',
|
||||
'remotes'
|
||||
)
|
||||
|
||||
|
||||
# Configuration file for gitolite
|
||||
GITOLITE_CONFIG = os.path.join(
|
||||
os.path.abspath(os.path.dirname(__file__)),
|
||||
'..',
|
||||
'gitolite.conf'
|
||||
)
|
||||
|
||||
# Configuration keys to specify where the upload folder is and what is its
|
||||
# name
|
||||
UPLOAD_FOLDER = 'releases/'
|
||||
UPLOAD_FOLDER_PATH = './' + UPLOAD_FOLDER
|
||||
|
||||
# Home folder of the gitolite user -- Folder where to run gl-compile-conf from
|
||||
GITOLITE_HOME = None
|
||||
|
||||
# Version of gitolite used: 2 or 3?
|
||||
GITOLITE_VERSION = 3
|
||||
|
||||
# Folder containing all the public ssh keys for gitolite
|
||||
GITOLITE_KEYDIR = None
|
||||
|
||||
# Path to the gitolite.rc file
|
||||
GL_RC = None
|
||||
# Path to the /bin directory where the gitolite tools can be found
|
||||
GL_BINDIR = None
|
||||
|
||||
|
||||
#SMTP settings
|
||||
SMTP_SERVER = 'localhost'
|
||||
SMTP_PORT = 25
|
||||
SMTP_SSL = False
|
||||
|
||||
# Specify both for enabling SMTP auth
|
||||
SMTP_USERNAME = None
|
||||
SMTP_PASSWORD = None
|
||||
|
||||
|
||||
# Email used to sent emails
|
||||
FROM_EMAIL = 'pagure@pagure.org'
|
||||
|
||||
DOMAIN_EMAIL_NOTIFICATIONS = 'pagure.org'
|
||||
SALT_EMAIL = '<secret key to be changed>'
|
||||
|
||||
# Specify which authentication method to use, defaults to `fas` can be or
|
||||
# `local`
|
||||
# Default: ``fas``.
|
||||
PAGURE_AUTH = 'fas'
|
||||
|
||||
# When this is set to True, the session cookie will only be returned to the
|
||||
# server via ssl (https). If you connect to the server via plain http, the
|
||||
# cookie will not be sent. This prevents sniffing of the cookie contents.
|
||||
# This may be set to False when testing your application but should always
|
||||
# be set to True in production.
|
||||
# Default: ``True``.
|
||||
SESSION_COOKIE_SECURE = False
|
||||
SESSION_COOKIE_NAME = 'pagure'
|
||||
|
||||
# Boolean specifying wether to check the user's IP address when retrieving
|
||||
# its session. This make things more secure (thus is on by default) but
|
||||
# under certain setup it might not work (for example is there are proxies
|
||||
# in front of the application).
|
||||
CHECK_SESSION_IP = True
|
||||
|
||||
# Lenght for short commits ids or file hex
|
||||
SHORT_LENGTH = 6
|
||||
|
||||
# Used by SESSION_COOKIE_PATH
|
||||
APPLICATION_ROOT = '/'
|
||||
|
||||
# List of blacklisted project names
|
||||
BLACKLISTED_PROJECTS = [
|
||||
'static', 'pv', 'releases', 'new', 'api', 'settings',
|
||||
'logout', 'login', 'users', 'groups', 'projects', 'ssh_info']
|
||||
|
||||
# List of prefix allowed in project names
|
||||
ALLOWED_PREFIX = []
|
||||
|
||||
# List of blacklisted group names
|
||||
BLACKLISTED_GROUPS = ['forks']
|
||||
|
||||
|
||||
ACLS = {
|
||||
'create_project': 'Create a new project',
|
||||
'fork_project': 'Fork a project',
|
||||
'issue_assign': 'Assign issue to someone',
|
||||
'issue_create': 'Create a new ticket against this project',
|
||||
'issue_change_status': 'Change the status of a ticket of this project',
|
||||
'issue_comment': 'Comment on a ticket of this project',
|
||||
'pull_request_close': 'Close a pull-request of this project',
|
||||
'pull_request_comment': 'Comment on a pull-request of this project',
|
||||
'pull_request_flag': 'Flag a pull-request of this project',
|
||||
'pull_request_merge': 'Merge a pull-request of this project',
|
||||
}
|
69
pagure/doc/api.rst
Normal file
|
@ -0,0 +1,69 @@
|
|||
Authentication
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
To access some endpoints, you need to login to Pagure using API token. You
|
||||
can generate one in the project setting page.
|
||||
|
||||
When sending HTTP request, include an ``Authorization`` field in the header
|
||||
with value ``token $your-api-token``, where ``$your-api-token`` is the
|
||||
API token generated in the project setting page.
|
||||
|
||||
So the result should look like:
|
||||
|
||||
::
|
||||
|
||||
Authorization: token abcdefghijklmnop
|
||||
|
||||
Where ``abcdefghijklmnop`` is the API token provided by pagure.
|
||||
|
||||
Anyone with the token can access the APIs on your behalf, so please be
|
||||
sure to keep it private and safe.
|
||||
|
||||
Request Encoding
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
The payload of POST and GET requests is encoded as
|
||||
|
||||
``application/x-www-form-urlencoded``.
|
||||
|
||||
|
||||
This is an example URL of a GET request:
|
||||
|
||||
``https://pagure.io/api/0/test/issues?status=Open&tags=Pagure&tags=Enhancement``
|
||||
|
||||
|
||||
Return Encoding
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
The return value of API calls is ``application/json``. This is an
|
||||
example of return value:
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
"args": {
|
||||
"assignee": null,
|
||||
"author": null,
|
||||
"status": null,
|
||||
"tags": []
|
||||
},
|
||||
"issues": [
|
||||
{
|
||||
"assignee": null,
|
||||
"blocks": [],
|
||||
"comments": [],
|
||||
"content": "Sample ticket",
|
||||
"date_created": "1434266418",
|
||||
"depends": [],
|
||||
"id": 4,
|
||||
"private": false,
|
||||
"status": "Open",
|
||||
"tags": [],
|
||||
"title": "This is a sample",
|
||||
"user": {
|
||||
"fullname": "Pagure",
|
||||
"name": "API"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
122
pagure/doc_utils.py
Normal file
|
@ -0,0 +1,122 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
(c) 2014 - Copyright Red Hat Inc
|
||||
|
||||
Authors:
|
||||
Ralph Bean <rbean@redhat.com>
|
||||
Pierre-Yves Chibon <pingou@pingoured.fr>
|
||||
|
||||
"""
|
||||
|
||||
import docutils
|
||||
import docutils.core
|
||||
import docutils.examples
|
||||
import markupsafe
|
||||
import markdown
|
||||
import textwrap
|
||||
|
||||
|
||||
def modify_rst(rst, view_file_url=None):
|
||||
""" Downgrade some of our rst directives if docutils is too old. """
|
||||
if view_file_url:
|
||||
rst = rst.replace(
|
||||
'.. image:: ',
|
||||
'.. image:: %s' % view_file_url
|
||||
)
|
||||
|
||||
# We catch Exception if we want :-p
|
||||
# pylint: disable=W0703
|
||||
try:
|
||||
# The rst features we need were introduced in this version
|
||||
minimum = [0, 9]
|
||||
version = [int(cpt) for cpt in docutils.__version__.split('.')]
|
||||
|
||||
# If we're at or later than that version, no need to downgrade
|
||||
if version >= minimum:
|
||||
return rst
|
||||
except Exception: # pragma: no cover
|
||||
# If there was some error parsing or comparing versions, run the
|
||||
# substitutions just to be safe.
|
||||
pass
|
||||
|
||||
# On Fedora this will never work as the docutils version is to recent
|
||||
# Otherwise, make code-blocks into just literal blocks.
|
||||
substitutions = { # pragma: no cover
|
||||
'.. code-block:: javascript': '::',
|
||||
}
|
||||
|
||||
for old, new in substitutions.items(): # pragma: no cover
|
||||
rst = rst.replace(old, new)
|
||||
|
||||
return rst # pragma: no cover
|
||||
|
||||
|
||||
def modify_html(html):
|
||||
""" Perform style substitutions where docutils doesn't do what we want.
|
||||
"""
|
||||
|
||||
substitutions = {
|
||||
'<tt class="docutils literal">': '<code>',
|
||||
'</tt>': '</code>',
|
||||
}
|
||||
for old, new in substitutions.items():
|
||||
html = html.replace(old, new)
|
||||
|
||||
return html
|
||||
|
||||
|
||||
def convert_doc(rst_string, view_file_url=None):
|
||||
""" Utility to load an RST file and turn it into fancy HTML. """
|
||||
rst = modify_rst(rst_string, view_file_url)
|
||||
|
||||
overrides = {'report_level': 'quiet'}
|
||||
try:
|
||||
html = docutils.core.publish_parts(
|
||||
source=rst,
|
||||
writer_name='html',
|
||||
settings_overrides=overrides)
|
||||
except:
|
||||
return '<pre>%s</pre>' % rst
|
||||
|
||||
else:
|
||||
|
||||
html_string = html['html_body']
|
||||
|
||||
html_string = modify_html(html_string)
|
||||
|
||||
html_string = markupsafe.Markup(html_string)
|
||||
return html_string
|
||||
|
||||
|
||||
def convert_readme(content, ext, view_file_url=None):
|
||||
''' Convert the provided content according to the extension of the file
|
||||
provided.
|
||||
'''
|
||||
output = content
|
||||
safe = False
|
||||
if ext and ext in ['.rst']:
|
||||
safe = True
|
||||
output = convert_doc(content.decode('utf-8'), view_file_url)
|
||||
elif ext and ext in ['.mk', '.md', '.markdown']:
|
||||
output = markdown.markdown(content.decode('utf-8'))
|
||||
safe = True
|
||||
elif not ext or (ext and ext in ['.text', '.txt']):
|
||||
safe = True
|
||||
output = '<pre>%s</pre>' % content
|
||||
return output, safe
|
||||
|
||||
|
||||
def load_doc(endpoint):
|
||||
""" Utility to load an RST file and turn it into fancy HTML. """
|
||||
|
||||
rst = unicode(textwrap.dedent(endpoint.__doc__))
|
||||
|
||||
rst = modify_rst(rst)
|
||||
|
||||
api_docs = docutils.examples.html_body(rst)
|
||||
|
||||
api_docs = modify_html(api_docs)
|
||||
|
||||
api_docs = markupsafe.Markup(api_docs)
|
||||
return api_docs
|
183
pagure/docs_server.py
Normal file
|
@ -0,0 +1,183 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
(c) 2014-2015 - Copyright Red Hat Inc
|
||||
|
||||
Authors:
|
||||
Pierre-Yves Chibon <pingou@pingoured.fr>
|
||||
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
import flask
|
||||
import pygit2
|
||||
|
||||
import pagure.doc_utils
|
||||
import pagure.exceptions
|
||||
import pagure.lib
|
||||
import pagure.forms
|
||||
|
||||
# Create the application.
|
||||
APP = flask.Flask(__name__)
|
||||
|
||||
# set up FAS
|
||||
APP.config.from_object('pagure.default_config')
|
||||
|
||||
if 'PAGURE_CONFIG' in os.environ:
|
||||
APP.config.from_envvar('PAGURE_CONFIG')
|
||||
|
||||
SESSION = pagure.lib.create_session(APP.config['DB_URL'])
|
||||
|
||||
if not APP.debug:
|
||||
APP.logger.addHandler(pagure.mail_logging.get_mail_handler(
|
||||
smtp_server=APP.config.get('SMTP_SERVER', '127.0.0.1'),
|
||||
mail_admin=APP.config.get('MAIL_ADMIN', APP.config['EMAIL_ERROR'])
|
||||
))
|
||||
|
||||
# Send classic logs into syslog
|
||||
SHANDLER = logging.StreamHandler()
|
||||
SHANDLER.setLevel(APP.config.get('log_level', 'INFO'))
|
||||
APP.logger.addHandler(SHANDLER)
|
||||
|
||||
LOG = APP.logger
|
||||
|
||||
TMPL_HTML = '''
|
||||
<!DOCTYPE html>
|
||||
<html lang='en'>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
|
||||
<style type="text/css">
|
||||
ul {{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{content}
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
|
||||
|
||||
def __get_tree(repo_obj, tree, filepath, index=0, extended=False):
|
||||
''' Retrieve the entry corresponding to the provided filename in a
|
||||
given tree.
|
||||
'''
|
||||
filename = filepath[index]
|
||||
if isinstance(tree, pygit2.Blob): # pragma: no cover
|
||||
# If we were given a blob, then let's just return it
|
||||
return (tree, None, None)
|
||||
|
||||
for element in tree:
|
||||
if element.name == filename or \
|
||||
(not filename and element.name.startswith('index')):
|
||||
# If we have a folder we must go one level deeper
|
||||
if element.filemode == 16384:
|
||||
if (index + 1) == len(filepath):
|
||||
filepath.append('')
|
||||
return __get_tree(
|
||||
repo_obj, repo_obj[element.oid], filepath,
|
||||
index=index + 1, extended=True)
|
||||
else:
|
||||
return (element, tree, False)
|
||||
|
||||
if filename == '':
|
||||
return (None, tree, extended)
|
||||
else:
|
||||
raise pagure.exceptions.FileNotFoundException(
|
||||
'File %s not found' % ('/'.join(filepath),))
|
||||
|
||||
|
||||
def __get_tree_and_content(repo_obj, commit, path):
|
||||
''' Return the tree and the content of the specified file. '''
|
||||
|
||||
(blob_or_tree, tree_obj, extended) = __get_tree(
|
||||
repo_obj, commit.tree, path)
|
||||
|
||||
if blob_or_tree is None:
|
||||
return (tree_obj, None, False, extended)
|
||||
|
||||
if not repo_obj[blob_or_tree.oid]:
|
||||
# Not tested and no idea how to test it, but better safe than sorry
|
||||
flask.abort(404, 'File not found')
|
||||
|
||||
if isinstance(blob_or_tree, pygit2.TreeEntry): # Returned a file
|
||||
ext = os.path.splitext(blob_or_tree.name)[1]
|
||||
blob_obj = repo_obj[blob_or_tree.oid]
|
||||
content, safe = pagure.doc_utils.convert_readme(blob_obj.data, ext)
|
||||
|
||||
tree = sorted(tree_obj, key=lambda x: x.filemode)
|
||||
return (tree, content, safe, extended)
|
||||
|
||||
|
||||
@APP.route('/<repo>/')
|
||||
@APP.route('/<repo>/<path:filename>')
|
||||
@APP.route('/fork/<username>/<repo>/')
|
||||
@APP.route('/fork/<username>/<repo>/<path:filename>')
|
||||
def view_docs(repo, username=None, filename=None):
|
||||
""" Display the documentation
|
||||
"""
|
||||
|
||||
repo = pagure.lib.get_project(SESSION, repo, user=username)
|
||||
|
||||
if not repo:
|
||||
flask.abort(404, 'Project not found')
|
||||
|
||||
if not repo.settings.get('project_documentation', True):
|
||||
flask.abort(404, 'This project has documentation disabled')
|
||||
|
||||
reponame = os.path.join(APP.config['DOCS_FOLDER'], repo.path)
|
||||
if not os.path.exists(reponame):
|
||||
flask.abort(404, 'Documentation not found')
|
||||
|
||||
repo_obj = pygit2.Repository(reponame)
|
||||
|
||||
|
||||
if not repo_obj.is_empty:
|
||||
commit = repo_obj[repo_obj.head.target]
|
||||
else:
|
||||
flask.abort(404, 'No content found is the repository')
|
||||
branchname = 'master'
|
||||
|
||||
content = None
|
||||
tree = None
|
||||
safe = False
|
||||
if not filename:
|
||||
path = ['']
|
||||
else:
|
||||
path = [it for it in filename.split('/') if it]
|
||||
|
||||
if commit:
|
||||
try:
|
||||
(tree, content, safe, extended) = __get_tree_and_content(
|
||||
repo_obj, commit, path)
|
||||
if extended:
|
||||
filename += '/'
|
||||
except pagure.exceptions.FileNotFoundException as err:
|
||||
flask.flash(err.message, 'error')
|
||||
|
||||
mimetype = None
|
||||
if not filename:
|
||||
pass
|
||||
elif filename.endswith('.css'):
|
||||
mimetype = 'text/css'
|
||||
elif filename.endswith('.js'):
|
||||
mimetype = 'application/javascript'
|
||||
|
||||
if not content:
|
||||
if not tree or not len(tree):
|
||||
flask.abort(404, 'No content found is the repository')
|
||||
html = '<li>'
|
||||
for el in tree:
|
||||
name = el.name
|
||||
# Append a trailing '/' to the folders
|
||||
if el.filemode == 16384:
|
||||
name += '/'
|
||||
html += '<ul><a href="{0}">{1}</a></ul>'.format(el.name, name)
|
||||
html += '</li>'
|
||||
content = TMPL_HTML.format(content=html)
|
||||
|
||||
return flask.Response(content, mimetype=mimetype)
|
59
pagure/exceptions.py
Normal file
|
@ -0,0 +1,59 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
(c) 2014 - Copyright Red Hat Inc
|
||||
|
||||
Authors:
|
||||
Pierre-Yves Chibon <pingou@pingoured.fr>
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class PagureException(Exception):
|
||||
''' Parent class of all the exception for all Pagure specific
|
||||
exceptions.
|
||||
'''
|
||||
pass
|
||||
|
||||
|
||||
class RepoExistsException(PagureException):
|
||||
''' Exception thrown when trying to create a repository that already
|
||||
exists.
|
||||
'''
|
||||
pass
|
||||
|
||||
|
||||
class FileNotFoundException(PagureException):
|
||||
''' Exception thrown when trying to create a repository that already
|
||||
exists.
|
||||
'''
|
||||
pass
|
||||
|
||||
|
||||
class APIError(PagureException):
|
||||
''' Exception raised by the API when something goes wrong. '''
|
||||
|
||||
def __init__(self, status_code, error_code, error=None):
|
||||
self.status_code = status_code
|
||||
self.error_code = error_code
|
||||
self.error = error
|
||||
|
||||
|
||||
class BranchNotFoundException(PagureException):
|
||||
''' Exception thrown when trying to use a branch that could not be
|
||||
found in a repository.
|
||||
'''
|
||||
pass
|
||||
|
||||
|
||||
class PagureEvException(PagureException):
|
||||
''' Exceptions used in the pagure-stream-server.
|
||||
'''
|
||||
pass
|
||||
|
||||
|
||||
class GitConflictsException(PagureException):
|
||||
''' Exception used when trying to pull on a repo and that leads to
|
||||
conflicts.
|
||||
'''
|
||||
pass
|
423
pagure/forms.py
Normal file
|
@ -0,0 +1,423 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
(c) 2014 - Copyright Red Hat Inc
|
||||
|
||||
Authors:
|
||||
Pierre-Yves Chibon <pingou@pingoured.fr>
|
||||
|
||||
"""
|
||||
|
||||
import re
|
||||
from flask.ext import wtf
|
||||
import wtforms
|
||||
# pylint: disable=R0903,W0232,E1002
|
||||
|
||||
|
||||
STRICT_REGEX = '^[a-zA-Z0-9-_]+$'
|
||||
TAGS_REGEX = '^[a-zA-Z0-9-_, .]+$'
|
||||
PROJECT_NAME_REGEX = \
|
||||
'^[a-zA-z0-9_][a-zA-Z0-9-_]*(/?[a-zA-z0-9_][a-zA-Z0-9-_]+)?$'
|
||||
|
||||
|
||||
class ProjectFormSimplified(wtf.Form):
|
||||
''' Form to edit the description of a project. '''
|
||||
description = wtforms.TextField(
|
||||
'Description <span class="error">*</span>',
|
||||
[wtforms.validators.Required()]
|
||||
)
|
||||
url = wtforms.TextField(
|
||||
'URL',
|
||||
[wtforms.validators.optional()]
|
||||
)
|
||||
avatar_email = wtforms.TextField(
|
||||
'Avatar email',
|
||||
[wtforms.validators.optional()]
|
||||
)
|
||||
tags = wtforms.TextField(
|
||||
'Project tags',
|
||||
[wtforms.validators.optional()]
|
||||
)
|
||||
|
||||
|
||||
class ProjectForm(ProjectFormSimplified):
|
||||
''' Form to create or edit project. '''
|
||||
name = wtforms.TextField(
|
||||
'Project name <span class="error">*</span>',
|
||||
[
|
||||
wtforms.validators.Required(),
|
||||
wtforms.validators.Regexp(PROJECT_NAME_REGEX, flags=re.IGNORECASE)
|
||||
]
|
||||
)
|
||||
create_readme = wtforms.BooleanField(
|
||||
'Create README',
|
||||
[wtforms.validators.optional()],
|
||||
)
|
||||
|
||||
|
||||
class IssueFormSimplied(wtf.Form):
|
||||
''' Form to create or edit an issue. '''
|
||||
title = wtforms.TextField(
|
||||
'Title<span class="error">*</span>',
|
||||
[wtforms.validators.Required()]
|
||||
)
|
||||
issue_content = wtforms.TextAreaField(
|
||||
'Content<span class="error">*</span>',
|
||||
[wtforms.validators.Required()]
|
||||
)
|
||||
private = wtforms.BooleanField(
|
||||
'Private',
|
||||
[wtforms.validators.optional()],
|
||||
)
|
||||
|
||||
|
||||
class IssueForm(IssueFormSimplied):
|
||||
''' Form to create or edit an issue. '''
|
||||
status = wtforms.SelectField(
|
||||
'Status',
|
||||
[wtforms.validators.Required()],
|
||||
choices=[]
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
""" Calls the default constructor with the normal argument but
|
||||
uses the list of collection provided to fill the choices of the
|
||||
drop-down list.
|
||||
"""
|
||||
super(IssueForm, self).__init__(*args, **kwargs)
|
||||
if 'status' in kwargs:
|
||||
self.status.choices = [
|
||||
(status, status) for status in kwargs['status']
|
||||
]
|
||||
|
||||
|
||||
class RequestPullForm(wtf.Form):
|
||||
''' Form to create a request pull. '''
|
||||
title = wtforms.TextField(
|
||||
'Title<span class="error">*</span>',
|
||||
[wtforms.validators.Required()]
|
||||
)
|
||||
initial_comment = wtforms.TextAreaField(
|
||||
'Initial Comment', [wtforms.validators.Optional()])
|
||||
|
||||
|
||||
class RemoteRequestPullForm(RequestPullForm):
|
||||
''' Form to create a remote request pull. '''
|
||||
git_repo = wtforms.TextField(
|
||||
'Git repo address<span class="error">*</span>',
|
||||
[wtforms.validators.Required()]
|
||||
)
|
||||
branch_from = wtforms.TextField(
|
||||
'Git branch<span class="error">*</span>',
|
||||
[wtforms.validators.Required()]
|
||||
)
|
||||
branch_to = wtforms.TextField(
|
||||
'Git branch to merge in<span class="error">*</span>',
|
||||
[wtforms.validators.Required()]
|
||||
)
|
||||
|
||||
|
||||
class AddIssueTagForm(wtf.Form):
|
||||
''' Form to add a comment to an issue. '''
|
||||
tag = wtforms.TextField(
|
||||
'tag',
|
||||
[
|
||||
wtforms.validators.Optional(),
|
||||
wtforms.validators.Regexp(TAGS_REGEX, flags=re.IGNORECASE)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class StatusForm(wtf.Form):
|
||||
''' Form to add/change the status of an issue. '''
|
||||
status = wtforms.SelectField(
|
||||
'Status',
|
||||
[wtforms.validators.Required()],
|
||||
choices=[]
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
""" Calls the default constructor with the normal argument but
|
||||
uses the list of collection provided to fill the choices of the
|
||||
drop-down list.
|
||||
"""
|
||||
super(StatusForm, self).__init__(*args, **kwargs)
|
||||
if 'status' in kwargs:
|
||||
self.status.choices = [
|
||||
(status, status) for status in kwargs['status']
|
||||
]
|
||||
|
||||
|
||||
class NewTokenForm(wtf.Form):
|
||||
''' Form to add/change the status of an issue. '''
|
||||
acls = wtforms.SelectMultipleField(
|
||||
'ACLs',
|
||||
[wtforms.validators.Required()],
|
||||
choices=[]
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
""" Calls the default constructor with the normal argument but
|
||||
uses the list of collection provided to fill the choices of the
|
||||
drop-down list.
|
||||
"""
|
||||
super(NewTokenForm, self).__init__(*args, **kwargs)
|
||||
if 'acls' in kwargs:
|
||||
self.acls.choices = [
|
||||
(acl.name, acl.name) for acl in kwargs['acls']
|
||||
]
|
||||
|
||||
|
||||
class UpdateIssueForm(wtf.Form):
|
||||
''' Form to add a comment to an issue. '''
|
||||
tag = wtforms.TextField(
|
||||
'tag',
|
||||
[
|
||||
wtforms.validators.Optional(),
|
||||
wtforms.validators.Regexp(TAGS_REGEX, flags=re.IGNORECASE)
|
||||
]
|
||||
)
|
||||
depends = wtforms.TextField(
|
||||
'dependency issue', [wtforms.validators.Optional()]
|
||||
)
|
||||
blocks = wtforms.TextField(
|
||||
'blocked issue', [wtforms.validators.Optional()]
|
||||
)
|
||||
comment = wtforms.TextAreaField(
|
||||
'Comment', [wtforms.validators.Optional()]
|
||||
)
|
||||
assignee = wtforms.TextAreaField(
|
||||
'Assigned to', [wtforms.validators.Optional()]
|
||||
)
|
||||
status = wtforms.SelectField(
|
||||
'Status',
|
||||
[wtforms.validators.Optional()],
|
||||
choices=[]
|
||||
)
|
||||
priority = wtforms.SelectField(
|
||||
'Priority',
|
||||
[wtforms.validators.Optional()],
|
||||
choices=[]
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
""" Calls the default constructor with the normal argument but
|
||||
uses the list of collection provided to fill the choices of the
|
||||
drop-down list.
|
||||
"""
|
||||
super(UpdateIssueForm, self).__init__(*args, **kwargs)
|
||||
if 'status' in kwargs:
|
||||
self.status.choices = [
|
||||
(status, status) for status in kwargs['status']
|
||||
]
|
||||
|
||||
self.priority.choices = []
|
||||
if 'priorities' in kwargs:
|
||||
for key in sorted(kwargs['priorities']):
|
||||
self.priority.choices.append(
|
||||
(key, kwargs['priorities'][key])
|
||||
)
|
||||
|
||||
|
||||
class AddPullRequestCommentForm(wtf.Form):
|
||||
''' Form to add a comment to a pull-request. '''
|
||||
commit = wtforms.HiddenField('commit identifier')
|
||||
filename = wtforms.HiddenField('file changed')
|
||||
row = wtforms.HiddenField('row')
|
||||
requestid = wtforms.HiddenField('requestid')
|
||||
tree_id = wtforms.HiddenField('treeid')
|
||||
comment = wtforms.TextAreaField(
|
||||
'Comment<span class="error">*</span>',
|
||||
[wtforms.validators.Required()]
|
||||
)
|
||||
|
||||
|
||||
class AddPullRequestFlagForm(wtf.Form):
|
||||
''' Form to add a flag to a pull-request. '''
|
||||
username = wtforms.TextField(
|
||||
'Username', [wtforms.validators.Required()])
|
||||
percent = wtforms.TextField(
|
||||
'Percentage of completion', [wtforms.validators.Required()])
|
||||
comment = wtforms.TextAreaField(
|
||||
'Comment', [wtforms.validators.Required()])
|
||||
url = wtforms.TextField(
|
||||
'URL', [wtforms.validators.Required()])
|
||||
uid = wtforms.TextField(
|
||||
'UID', [wtforms.validators.optional()])
|
||||
|
||||
|
||||
class UserSettingsForm(wtf.Form):
|
||||
''' Form to create or edit project. '''
|
||||
ssh_key = wtforms.TextAreaField(
|
||||
'Public SSH key <span class="error">*</span>',
|
||||
[wtforms.validators.Required()]
|
||||
)
|
||||
|
||||
|
||||
class AddUserForm(wtf.Form):
|
||||
''' Form to add a user to a project. '''
|
||||
user = wtforms.TextField(
|
||||
'Username <span class="error">*</span>',
|
||||
[wtforms.validators.Required()]
|
||||
)
|
||||
|
||||
|
||||
class AssignIssueForm(wtf.Form):
|
||||
''' Form to assign an user to an issue. '''
|
||||
assignee = wtforms.TextField(
|
||||
'Assignee <span class="error">*</span>',
|
||||
[wtforms.validators.Required()]
|
||||
)
|
||||
|
||||
|
||||
class AddGroupForm(wtf.Form):
|
||||
''' Form to add a group to a project. '''
|
||||
group = wtforms.TextField(
|
||||
'Group <span class="error">*</span>',
|
||||
[
|
||||
wtforms.validators.Required(),
|
||||
wtforms.validators.Regexp(STRICT_REGEX, flags=re.IGNORECASE)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class ConfirmationForm(wtf.Form):
|
||||
''' Simple form used just for CSRF protection. '''
|
||||
pass
|
||||
|
||||
|
||||
class UploadFileForm(wtf.Form):
|
||||
''' Form to upload a file. '''
|
||||
filestream = wtforms.FileField(
|
||||
'File',
|
||||
[wtforms.validators.Required()])
|
||||
|
||||
|
||||
class UserEmailForm(wtf.Form):
|
||||
''' Form to edit the description of a project. '''
|
||||
email = wtforms.TextField(
|
||||
'email', [wtforms.validators.Required()]
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(UserEmailForm, self).__init__(*args, **kwargs)
|
||||
if 'emails' in kwargs:
|
||||
if kwargs['emails']:
|
||||
self.email.validators.append(
|
||||
wtforms.validators.NoneOf(kwargs['emails'])
|
||||
)
|
||||
else:
|
||||
self.email.validators = [wtforms.validators.Required()]
|
||||
|
||||
|
||||
class ProjectCommentForm(wtf.Form):
|
||||
''' Form to represent project. '''
|
||||
objid = wtforms.TextField(
|
||||
'Ticket/Request id',
|
||||
[wtforms.validators.Required()]
|
||||
)
|
||||
useremail = wtforms.TextField(
|
||||
'Email',
|
||||
[wtforms.validators.Required()]
|
||||
)
|
||||
|
||||
|
||||
class CommentForm(wtf.Form):
|
||||
''' Form to upload a file. '''
|
||||
comment = wtforms.FileField(
|
||||
'Comment',
|
||||
[wtforms.validators.Required()])
|
||||
|
||||
|
||||
class NewGroupForm(wtf.Form):
|
||||
""" Form to ask for a password change. """
|
||||
group_name = wtforms.TextField(
|
||||
'Group name <span class="error">*</span>',
|
||||
[
|
||||
wtforms.validators.Required(),
|
||||
wtforms.validators.Length(max=16),
|
||||
wtforms.validators.Regexp(STRICT_REGEX, flags=re.IGNORECASE)
|
||||
]
|
||||
)
|
||||
group_type = wtforms.SelectField(
|
||||
'Group type',
|
||||
[wtforms.validators.Required()],
|
||||
choices=[]
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
""" Calls the default constructor with the normal argument but
|
||||
uses the list of collection provided to fill the choices of the
|
||||
drop-down list.
|
||||
"""
|
||||
super(NewGroupForm, self).__init__(*args, **kwargs)
|
||||
if 'group_types' in kwargs:
|
||||
self.group_type.choices = [
|
||||
(grptype, grptype) for grptype in kwargs['group_types']
|
||||
]
|
||||
|
||||
|
||||
class EditFileForm(wtf.Form):
|
||||
""" Form used to edit a file. """
|
||||
content = wtforms.TextAreaField(
|
||||
'content', [wtforms.validators.Required()])
|
||||
commit_title = wtforms.TextField(
|
||||
'Title', [wtforms.validators.Required()])
|
||||
commit_message = wtforms.TextAreaField(
|
||||
'Commit message', [wtforms.validators.optional()])
|
||||
email = wtforms.SelectField(
|
||||
'Email', [wtforms.validators.Required()],
|
||||
choices=[]
|
||||
)
|
||||
branch = wtforms.TextField(
|
||||
'Branch', [wtforms.validators.Required()])
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
""" Calls the default constructor with the normal argument but
|
||||
uses the list of collection provided to fill the choices of the
|
||||
drop-down list.
|
||||
"""
|
||||
super(EditFileForm, self).__init__(*args, **kwargs)
|
||||
if 'emails' in kwargs:
|
||||
self.email.choices = [
|
||||
(email.email, email.email) for email in kwargs['emails']
|
||||
]
|
||||
|
||||
|
||||
class DefaultBranchForm(wtf.Form):
|
||||
"""Form to change the default branh for a repository"""
|
||||
branches = wtforms.SelectField(
|
||||
'default_branch',
|
||||
[wtforms.validators.Required()],
|
||||
choices=[]
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
""" Calls the default constructor with the normal argument but
|
||||
uses the list of collection provided to fill the choices of the
|
||||
drop-down list.
|
||||
"""
|
||||
super(DefaultBranchForm, self).__init__(*args, **kwargs)
|
||||
if 'branches' in kwargs:
|
||||
self.branches.choices = [
|
||||
(branch, branch) for branch in kwargs['branches']
|
||||
]
|
||||
|
||||
class EditCommentForm(wtf.Form):
|
||||
""" Form to verify that comment is not empty
|
||||
"""
|
||||
update_comment = wtforms.TextAreaField(
|
||||
'Comment<span class="error">*</span>',
|
||||
[wtforms.validators.Required()]
|
||||
)
|
||||
|
||||
|
||||
class ForkRepoForm(wtf.Form):
|
||||
''' Form to fork a project in the API. '''
|
||||
repo = wtforms.TextField(
|
||||
'The project name <span class="error">*</span>',
|
||||
[wtforms.validators.Required()]
|
||||
)
|
||||
username = wtforms.TextField(
|
||||
'User who forked the project',
|
||||
[wtforms.validators.optional()])
|
123
pagure/hooks/__init__.py
Normal file
|
@ -0,0 +1,123 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
(c) 2014 - Copyright Red Hat Inc
|
||||
|
||||
Authors:
|
||||
Pierre-Yves Chibon <pingou@pingoured.fr>
|
||||
|
||||
"""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import wtforms
|
||||
|
||||
from pagure.exceptions import FileNotFoundException
|
||||
from pagure import APP, get_repo_path
|
||||
|
||||
|
||||
class RequiredIf(wtforms.validators.Required):
|
||||
""" Wtforms validator setting a field as required if another field
|
||||
has a value.
|
||||
"""
|
||||
|
||||
def __init__(self, fields, *args, **kwargs):
|
||||
if isinstance(fields, basestring):
|
||||
fields = [fields]
|
||||
self.fields = fields
|
||||
super(RequiredIf, self).__init__(*args, **kwargs)
|
||||
|
||||
def __call__(self, form, field):
|
||||
for fieldname in self.fields:
|
||||
nfield = form._fields.get(fieldname)
|
||||
if nfield is None:
|
||||
raise Exception(
|
||||
'no field named "%s" in form' % fieldname)
|
||||
if bool(nfield.data):
|
||||
super(RequiredIf, self).__call__(form, field)
|
||||
|
||||
|
||||
class BaseHook(object):
|
||||
''' Base class for pagure's hooks. '''
|
||||
|
||||
name = None
|
||||
form = None
|
||||
description = None
|
||||
hook_type = 'post-receive'
|
||||
|
||||
@classmethod
|
||||
def set_up(cls, project):
|
||||
''' Install the generic post-receive hook that allow us to call
|
||||
multiple post-receive hooks as set per plugin.
|
||||
'''
|
||||
repopaths = [get_repo_path(project)]
|
||||
for folder in [
|
||||
APP.config.get('DOCS_FOLDER'),
|
||||
APP.config.get('REQUESTS_FOLDER')]:
|
||||
repopaths.append(
|
||||
os.path.join(folder, project.path)
|
||||
)
|
||||
|
||||
hook_files = os.path.join(
|
||||
os.path.dirname(os.path.realpath(__file__)), 'files')
|
||||
|
||||
for repopath in repopaths:
|
||||
# Make sure the hooks folder exists
|
||||
hookfolder = os.path.join(repopath, 'hooks')
|
||||
if not os.path.exists(hookfolder):
|
||||
os.makedirs(hookfolder)
|
||||
|
||||
# Install the main post-receive file
|
||||
postreceive = os.path.join(hookfolder, cls.hook_type)
|
||||
if not os.path.exists(postreceive):
|
||||
os.symlink(os.path.join(hook_files, cls.hook_type),
|
||||
postreceive)
|
||||
|
||||
@classmethod
|
||||
def base_install(cls, repopaths, dbobj, hook_name, filein):
|
||||
''' Method called to install the hook for a project.
|
||||
|
||||
:arg project: a ``pagure.model.Project`` object to which the hook
|
||||
should be installed
|
||||
:arg dbobj: the DB object the hook uses to store the settings
|
||||
information.
|
||||
|
||||
'''
|
||||
for repopath in repopaths:
|
||||
if not os.path.exists(repopath):
|
||||
raise FileNotFoundException('Repo %s not found' % repopath)
|
||||
|
||||
hook_files = os.path.join(
|
||||
os.path.dirname(os.path.realpath(__file__)), 'files')
|
||||
|
||||
# Make sure the hooks folder exists
|
||||
hookfolder = os.path.join(repopath, 'hooks')
|
||||
if not os.path.exists(hookfolder):
|
||||
os.makedirs(hookfolder)
|
||||
|
||||
# Install the hook itself
|
||||
hook_file = os.path.join(repopath, 'hooks', cls.hook_type + '.'
|
||||
+ hook_name)
|
||||
|
||||
if not os.path.exists(hook_file):
|
||||
os.symlink(
|
||||
os.path.join(hook_files, filein),
|
||||
hook_file
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def base_remove(cls, repopaths, hook_name):
|
||||
''' Method called to remove the hook of a project.
|
||||
|
||||
:arg project: a ``pagure.model.Project`` object to which the hook
|
||||
should be installed
|
||||
|
||||
'''
|
||||
for repopath in repopaths:
|
||||
if not os.path.exists(repopath):
|
||||
raise FileNotFoundException('Repo %s not found' % repopath)
|
||||
|
||||
hook_path = os.path.join(repopath, 'hooks', cls.hook_type + '.'
|
||||
+ hook_name)
|
||||
if os.path.exists(hook_path):
|
||||
os.unlink(hook_path)
|
90
pagure/hooks/fedmsg.py
Normal file
|
@ -0,0 +1,90 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
(c) 2015 - Copyright Red Hat Inc
|
||||
|
||||
Authors:
|
||||
Pierre-Yves Chibon <pingou@pingoured.fr>
|
||||
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
import sqlalchemy as sa
|
||||
import wtforms
|
||||
from flask.ext import wtf
|
||||
from sqlalchemy.orm import relation
|
||||
from sqlalchemy.orm import backref
|
||||
|
||||
from pagure.hooks import BaseHook
|
||||
from pagure.lib.model import BASE, Project
|
||||
from pagure import get_repo_path
|
||||
|
||||
|
||||
class FedmsgTable(BASE):
|
||||
""" Stores information about the fedmsg hook deployed on a project.
|
||||
|
||||
Table -- hook_fedmsg
|
||||
"""
|
||||
|
||||
__tablename__ = 'hook_fedmsg'
|
||||
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
project_id = sa.Column(
|
||||
sa.Integer,
|
||||
sa.ForeignKey(
|
||||
'projects.id', onupdate='CASCADE', ondelete='CASCADE'),
|
||||
nullable=False,
|
||||
unique=True,
|
||||
index=True)
|
||||
|
||||
active = sa.Column(sa.Boolean, nullable=False, default=False)
|
||||
|
||||
project = relation(
|
||||
'Project', remote_side=[Project.id],
|
||||
backref=backref(
|
||||
'fedmsg_hook', cascade="delete, delete-orphan",
|
||||
single_parent=True)
|
||||
)
|
||||
|
||||
|
||||
class FedmsgForm(wtf.Form):
|
||||
''' Form to configure the fedmsg hook. '''
|
||||
active = wtforms.BooleanField(
|
||||
'Active',
|
||||
[wtforms.validators.Optional()]
|
||||
)
|
||||
|
||||
|
||||
class Fedmsg(BaseHook):
|
||||
''' Fedmsg hooks. '''
|
||||
|
||||
name = 'Fedmsg'
|
||||
description = 'This hook pushes the commit messages'\
|
||||
' to the Fedora bus to be consumed by other applications.'
|
||||
form = FedmsgForm
|
||||
db_object = FedmsgTable
|
||||
backref = 'fedmsg_hook'
|
||||
form_fields = ['active']
|
||||
|
||||
@classmethod
|
||||
def install(cls, project, dbobj):
|
||||
''' Method called to install the hook for a project.
|
||||
|
||||
:arg project: a ``pagure.model.Project`` object to which the hook
|
||||
should be installed
|
||||
|
||||
'''
|
||||
repopaths = [get_repo_path(project)]
|
||||
cls.base_install(repopaths, dbobj, 'fedmsg', 'fedmsg_hook.py')
|
||||
|
||||
@classmethod
|
||||
def remove(cls, project):
|
||||
''' Method called to remove the hook of a project.
|
||||
|
||||
:arg project: a ``pagure.model.Project`` object to which the hook
|
||||
should be installed
|
||||
|
||||
'''
|
||||
repopaths = [get_repo_path(project)]
|
||||
cls.base_remove(repopaths, 'fedmsg')
|
91
pagure/hooks/files/fedmsg_hook.py
Executable file
|
@ -0,0 +1,91 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import getpass
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
import fedmsg
|
||||
import fedmsg.config
|
||||
|
||||
|
||||
if 'PAGURE_CONFIG' not in os.environ \
|
||||
and os.path.exists('/etc/pagure/pagure.cfg'):
|
||||
os.environ['PAGURE_CONFIG'] = '/etc/pagure/pagure.cfg'
|
||||
|
||||
import pagure
|
||||
import pagure.lib.git
|
||||
|
||||
abspath = os.path.abspath(os.environ['GIT_DIR'])
|
||||
|
||||
|
||||
print "Emitting a message to the fedmsg bus."
|
||||
config = fedmsg.config.load_config([], None)
|
||||
config['active'] = True
|
||||
config['endpoints']['relay_inbound'] = config['relay_inbound']
|
||||
fedmsg.init(name='relay_inbound', **config)
|
||||
|
||||
|
||||
seen = []
|
||||
|
||||
# Read in all the rev information git-receive-pack hands us.
|
||||
for line in sys.stdin.readlines():
|
||||
(oldrev, newrev, refname) = line.strip().split(' ', 2)
|
||||
|
||||
forced = False
|
||||
if set(newrev) == set(['0']):
|
||||
print "Deleting a reference/branch, so we won't run the "\
|
||||
"pagure hook"
|
||||
break
|
||||
elif set(oldrev) == set(['0']):
|
||||
print "New reference/branch"
|
||||
oldrev = '^%s' % oldrev
|
||||
elif pagure.lib.git.is_forced_push(oldrev, newrev, abspath):
|
||||
forced = True
|
||||
base = pagure.lib.git.get_base_revision(oldrev, newrev, abspath)
|
||||
if base:
|
||||
oldrev = base[0]
|
||||
|
||||
revs = pagure.lib.git.get_revs_between(
|
||||
oldrev, newrev, abspath, refname, forced=forced)
|
||||
project_name = pagure.lib.git.get_repo_name(abspath)
|
||||
username = pagure.lib.git.get_username(abspath)
|
||||
project = pagure.lib.get_project(pagure.SESSION, project_name, username)
|
||||
if not project:
|
||||
project = project_name
|
||||
|
||||
auths = set()
|
||||
for rev in revs:
|
||||
email = pagure.lib.git.get_author_email(rev, abspath)
|
||||
name = pagure.lib.git.get_author(rev, abspath)
|
||||
author = pagure.lib.search_user(pagure.SESSION, email=email) or name
|
||||
auths.add(author)
|
||||
|
||||
authors = []
|
||||
for author in auths:
|
||||
if isinstance(author, basestring):
|
||||
author = author
|
||||
else:
|
||||
author = author.to_json(public=True)
|
||||
authors.append(author)
|
||||
|
||||
if revs:
|
||||
revs.reverse()
|
||||
print "* Publishing information for %i commits" % len(revs)
|
||||
pagure.lib.notify.log(
|
||||
project=project,
|
||||
topic="git.receive",
|
||||
msg=dict(
|
||||
total_commits=len(revs),
|
||||
start_commit=revs[0],
|
||||
end_commit=revs[-1],
|
||||
branch=refname,
|
||||
forced=forced,
|
||||
authors=list(authors),
|
||||
agent=os.environ['GL_USER'],
|
||||
repo=project.to_json(public=True)
|
||||
if not isinstance(project, basestring) else project,
|
||||
),
|
||||
)
|