code:
#!/bin/bash
#
#Copyright (C) 2017 avideo authors (see AUTHORS)
#
#    This file is part of AVideo.
#
#    AVideo 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 3 of the License, or
#    (at your option) any later version.
#
#    AVideo 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 AVideo.  If not, see <http://www.gnu.org/licenses/>.

zip 2&> /dev/null
if [ $? == 127 ]; then
	#The zip command isn't available; default to a Python re-implementation of the task to be performed
	python -c "`cat $0 | tail -15`"
else
	zip --quiet youtube-dl youtube_dl/*.py youtube_dl/*/*.py
	zip --quiet --junk-paths youtube-dl youtube_dl/__main__.py
fi
exit

#A python re-implementation of the 'zips'
import os, zipfile
with zipfile.ZipFile('youtube-dl.zip', mode='w', compression=zipfile.ZIP_DEFLATED) as exezip:
	for pkgel in os.listdir('youtube_dl'):
		path = 'youtube_dl/'+pkgel
		if os.path.isfile(path):
			exezip.write(path)
		else:
			for pkgfile in os.listdir(path):
				exezip.write(path+'/'+pkgfile)
	exezip.write('youtube_dl/__main__.py', arcname='__main__.py')\


code:
#youtube_dl/extractor/youtube.py: Signature decryption invoking js/swf sig parsers
#ln=
        if player_type == 'js':
            code = self._download_webpage(
                player_url, video_id,
                note=download_note,
                errnote='Download of %s failed' % player_url)
            res = self._parse_sig_js(code)
        elif player_type == 'swf':
            urlh = self._request_webpage(
                player_url, video_id,
                note=download_note,
                errnote='Download of %s failed' % player_url)
            code = urlh.read()
            res = self._parse_sig_swf(code)

repo_name: GPast/avideo

license: mit


code:
#youtube_dl/extractor/youtube.py: The js/swf sig parsers
#ln=\z
    def _parse_sig_js(self, jscode):
        funcname = self._search_regex(
            (r'(["\'])signature\1\s*,\s*(?P<sig>[a-zA-Z0-9$]+)\(',
             r'\.sig\|\|(?P<sig>[a-zA-Z0-9$]+)\('),
            jscode, 'Initial JS player signature function name', group='sig')

        jsi = JSInterpreter(jscode)
        initial_function = jsi.extract_function(funcname)
        return lambda s: initial_function([s])

    def _parse_sig_swf(self, file_contents):
        swfi = SWFInterpreter(file_contents)
        TARGET_CLASSNAME = 'SignatureDecipher'
        searched_class = swfi.extract_class(TARGET_CLASSNAME)
        initial_function = swfi.extract_function(searched_class, 'decipher')
        return lambda s: initial_function([s])


code:
#devscripts/run_tests.sh: Remove the iQiyi interpreter test
#ln=\z
|iqiyi_sdk_interpreter


code:
#youtube_dl/extractor/iqiyi.py: Login function, invoking non-free client side authentication algorithm
#ln=
    def _login(self):
        (username, password) = self._get_login_info()

        # No authentication to be performed
        if not username:
            return True

        data = self._download_json(
            'http://kylin.iqiyi.com/get_token', None,
            note='Get token for logging', errnote='Unable to get token for logging')
        sdk = data['sdk']
        timestamp = int(time.time())
        target = '/apis/reglogin/login.action?lang=zh_TW&area_code=null&email=%s&passwd=%s&agenttype=1&from=undefined&keeplogin=0&piccode=&fromurl=&_pos=1' % (
            username, self._rsa_fun(password.encode('utf-8')))

        interp = IqiyiSDKInterpreter(sdk)
        sign = interp.run(target, data['ip'], timestamp)

        validation_params = {
            'target': target,
            'server': 'BEA3AA1908656AABCCFF76582C4C6660',
            'token': data['token'],
            'bird_src': 'f8d91d57af224da7893dd397d52d811a',
            'sign': sign,
            'bird_t': timestamp,
        }
        validation_result = self._download_json(
            'http://kylin.iqiyi.com/validate?' + compat_urllib_parse_urlencode(validation_params), None,
            note='Validate credentials', errnote='Unable to validate credentials')

        MSG_MAP = {
            'P00107': 'please login via the web interface and enter the CAPTCHA code',
            'P00117': 'bad username or password',
        }

        code = validation_result['code']
        if code != 'A00000':
            msg = MSG_MAP.get(code)
            if not msg:
                msg = 'error %s' % code
                if validation_result.get('msg'):
                    msg += ': ' + validation_result['msg']
            self._downloader.report_warning('unable to log in: ' + msg)
            return False

        return True


code:
#devscripts/release.sh: Version determination for youtube-dl
#ln=
if [ -z "$1" ]; then echo "ERROR: specify version number like this: $0 1994.09.06"; exit 1; fi
version="$1"
major_version=$(echo "$version" | sed -n 's#^\([0-9]*\.[0-9]*\.[0-9]*\).*#\1#p')
if test "$major_version" '!=' "$(date '+%Y.%m.%d')"; then
    echo "$version does not start with today's date!"
    exit 1
fi


code:
#Makefile: creates the executable zipfile
#ln=

youtube-dl: youtube_dl/*.py youtube_dl/*/*.py
	mkdir -p zip
	for d in youtube_dl youtube_dl/downloader youtube_dl/extractor youtube_dl/postprocessor ; do \
	  mkdir -p zip/$$d ;\
	  cp -pPR $$d/*.py zip/$$d/ ;\
	done
	touch -t 200001010101 zip/youtube_dl/*.py zip/youtube_dl/*/*.py
	mv zip/youtube_dl/__main__.py zip/
	cd zip ; zip -q ../youtube-dl youtube_dl/*.py youtube_dl/*/*.py __main__.py
	rm -rf zip
	echo '#!$(PYTHON)' > youtube-dl
	cat youtube-dl.zip >> youtube-dl
	rm youtube-dl.zip
	chmod a+x youtube-dl

README.md: youtube_dl/*.py youtube_dl/*/*.py


code:
#Makefile: Some of the files to go into the tarball
#ln=COPYING.txt PLEASENOTE.txt README.md README.txt \
ChangeLog LICENSE README.md README.txt \


code:
#.travis.yml: Dev e-mails
#ln=
  email:
    - filippo.valsorda@gmail.com
    - yasoob.khld@gmail.com


code:
#Makefile: some files which really, really do not need to be cleaned away
#ln=youtube-dl.py youtube_dl.lib BINFILES/*
ISSUE_TEMPLATE.md.tmp youtube-dl youtube-dl.exe


code:
#devscripts/release.sh: Version updating, alreading covered by version.sh
#ln=\z

/bin/echo -e "\n### Changing version in version.py..."
sed -i "s/__version__ = '.*'/__version__ = '$version'/" youtube_dl/version.py

/bin/echo -e "\n### Changing version in ChangeLog..."
sed -i "s/<unreleased>/$version/" ChangeLog


code:
#Makefile: Some of the files to go into the tarball
#ln=\n\t\tmv youtube-dl.tar.gz BINFILES/
\
		youtube-dl


code:
#Makefile: prerequisites for the tarball
#ln=BINFILES/youtube-dl.tar.gz:
youtube-dl.tar.gz: youtube-dl


code:
#youtube_dl/options.py: A reference to MS Windows usage in the options data, which is not supported here
#ln=)
'(%APPDATA%/youtube-dl/config.txt on Windows)')


code:
#Makefile: youtube-dl is now actually "youtube-dl.py"
#ln=all: README.md
all: youtube-dl README.md


code:
#youtube_dl/extractor/iqiyi.py: interpreter for "iQiyi SDK"
#ln=\z
class IqiyiSDK(object):
    def __init__(self, target, ip, timestamp):
        self.target = target
        self.ip = ip
        self.timestamp = timestamp

    @staticmethod
    def split_sum(data):
        return compat_str(sum(map(lambda p: int(p, 16), list(data))))

    @staticmethod
    def digit_sum(num):
        if isinstance(num, int):
            num = compat_str(num)
        return compat_str(sum(map(int, num)))

    def even_odd(self):
        even = self.digit_sum(compat_str(self.timestamp)[::2])
        odd = self.digit_sum(compat_str(self.timestamp)[1::2])
        return even, odd

    def preprocess(self, chunksize):
        self.target = md5_text(self.target)
        chunks = []
        for i in range(32 // chunksize):
            chunks.append(self.target[chunksize * i:chunksize * (i + 1)])
        if 32 % chunksize:
            chunks.append(self.target[32 - 32 % chunksize:])
        return chunks, list(map(int, self.ip.split('.')))

    def mod(self, modulus):
        chunks, ip = self.preprocess(32)
        self.target = chunks[0] + ''.join(map(lambda p: compat_str(p % modulus), ip))

    def split(self, chunksize):
        modulus_map = {
            4: 256,
            5: 10,
            8: 100,
        }

        chunks, ip = self.preprocess(chunksize)
        ret = ''
        for i in range(len(chunks)):
            ip_part = compat_str(ip[i] % modulus_map[chunksize]) if i < 4 else ''
            if chunksize == 8:
                ret += ip_part + chunks[i]
            else:
                ret += chunks[i] + ip_part
        self.target = ret

    def handle_input16(self):
        self.target = md5_text(self.target)
        self.target = self.split_sum(self.target[:16]) + self.target + self.split_sum(self.target[16:])

    def handle_input8(self):
        self.target = md5_text(self.target)
        ret = ''
        for i in range(4):
            part = self.target[8 * i:8 * (i + 1)]
            ret += self.split_sum(part) + part
        self.target = ret

    def handleSum(self):
        self.target = md5_text(self.target)
        self.target = self.split_sum(self.target) + self.target

    def date(self, scheme):
        self.target = md5_text(self.target)
        d = time.localtime(self.timestamp)
        strings = {
            'y': compat_str(d.tm_year),
            'm': '%02d' % d.tm_mon,
            'd': '%02d' % d.tm_mday,
        }
        self.target += ''.join(map(lambda c: strings[c], list(scheme)))

    def split_time_even_odd(self):
        even, odd = self.even_odd()
        self.target = odd + md5_text(self.target) + even

    def split_time_odd_even(self):
        even, odd = self.even_odd()
        self.target = even + md5_text(self.target) + odd

    def split_ip_time_sum(self):
        chunks, ip = self.preprocess(32)
        self.target = compat_str(sum(ip)) + chunks[0] + self.digit_sum(self.timestamp)

    def split_time_ip_sum(self):
        chunks, ip = self.preprocess(32)
        self.target = self.digit_sum(self.timestamp) + chunks[0] + compat_str(sum(ip))

class IqiyiSDKInterpreter(object):
    def __init__(self, sdk_code):
        self.sdk_code = sdk_code

    def run(self, target, ip, timestamp):
        self.sdk_code = decode_packed_codes(self.sdk_code)

        functions = re.findall(r'input=([a-zA-Z0-9]+)\(input', self.sdk_code)

        sdk = IqiyiSDK(target, ip, timestamp)

        other_functions = {
            'handleSum': sdk.handleSum,
            'handleInput8': sdk.handle_input8,
            'handleInput16': sdk.handle_input16,
            'splitTimeEvenOdd': sdk.split_time_even_odd,
            'splitTimeOddEven': sdk.split_time_odd_even,
            'splitIpTimeSum': sdk.split_ip_time_sum,
            'splitTimeIpSum': sdk.split_time_ip_sum,
        }
        for function in functions:
            if re.match(r'mod\d+', function):
                sdk.mod(int(function[3:]))
            elif re.match(r'date[ymd]{3}', function):
                sdk.date(function[4:])
            elif re.match(r'split\d+', function):
                sdk.split(int(function[5:]))
            elif function in other_functions:
                other_functions[function]()
            else:
                raise ExtractorError('Unknown funcion %s' % function)

        return sdk.target


code:
#Makefile: remove yet ANOTHER reference to code testing
#ln=offlinetest:
offlinetest: codetest


code:
#Makefile: Point the installer to youtube-dl.py
#ln=
install: youtube-dl youtube-dl.1 youtube-dl.bash-completion youtube-dl.zsh youtube-dl.fish
	install -d $(DESTDIR)$(BINDIR)
	install -m 755 youtube-dl $(DESTDIR)$(BINDIR)


code:
#Makefile: fix references to the tarball (now in BINFILES)
#ln=tar: BINFILES/youtube-dl.tar.gz
tar: youtube-dl.tar.gz


code:
#Makefile: add a 'libdir' and change the python version
#ln=LIBDIR ?= $(PREFIX)/lib/python2.7/dist-packages\nPYTHON ?= /usr/bin/env python
PYTHON ?= /usr/bin/env python


code:
#test/test_age_restriction.py: Find some NSFW stuff without signature encryption...
#ln=2ERO94Ujrno
07FYdnEawAQ


code:
#setup.py: Contact and license details for youtube-dl
#ln=
    author='Ricardo Garcia',
    author_email='ytdl@yt-dl.org',
    maintainer='Sergey M.',
    maintainer_email='dstftw@gmail.com',


code:
#youtube_dl/__init__.py: Fix the license!
#ln=__license__ = 'GPLv3'
__license__ = 'Public Domain'


code:
#youtube_dl/__init__.py: No updater required
#ln=\z

from .update import update_self


code:
#youtube_dl/__init__.py: Eliminate the updater
#ln=\z
        # Update version
        if opts.update_self:
            update_self(ydl.to_screen, opts.verbose, ydl._opener)


code:
#youtube_dl/__init__.py: Eliminate the updater
#ln=if opts.rm_cachedir or opts.verbose:
if opts.update_self or opts.rm_cachedir:


code:
#youtube_dl/options.py: Remove the updater
#ln=\z

    general.add_option(
        '-v', '--version',
        action='version',
        help='Print program version and exit')
    general.add_option(
        '-U', '--update',
        action='store_true', dest='update_self',
        help='Update this program to latest version. Make sure that you have sufficient permissions (run with sudo if needed)')


code:
#setup.py: Incorrect license!
#ln=\z

        'License :: Public Domain',


code:
#Makefile: some code testing routine
#ln=
codetest:
	flake8 .


code:
#Makefile: invocation for code testing routine
#ln=\z

	$(MAKE) codetest


code:
#Makefile: GitHub Issue Template make:
#ln=\z

.github/ISSUE_TEMPLATE.md: devscripts/make_issue_template.py .github/ISSUE_TEMPLATE_tmpl.md  youtube_dl/version.py
	$(PYTHON) devscripts/make_issue_template.py .github/ISSUE_TEMPLATE_tmpl.md .github/ISSUE_TEMPLATE.md


code:
#setup.py: Some Py2EXE setup stuff
#ln=\nif False:\n\tNone

py2exe_console = [{
    'script': './youtube_dl/__main__.py',
    'dest_base': 'youtube-dl',
    'version': __version__,
    'description': DESCRIPTION,
    'comments': LONG_DESCRIPTION,
    'product_name': 'youtube-dl',
    'product_version': __version__,
}]

py2exe_params = {
    'console': py2exe_console,
    'options': {'py2exe': py2exe_options},
    'zipfile': None
}

if len(sys.argv) >= 2 and sys.argv[1] == 'py2exe':
    params = py2exe_params


code:
#setup.py: Even more Py2EXE setup stuff:
#ln=\z

try:
    # This will create an exe that needs Microsoft Visual C++ 2008
    # Redistributable Package
    import py2exe
except ImportError:
    if len(sys.argv) >= 2 and sys.argv[1] == 'py2exe':
        print('Cannot import py2exe', file=sys.stderr)
        exit(1)

py2exe_options = {
    'bundle_files': 1,
    'compressed': 1,
    'optimize': 2,
    'dist_dir': '.',
    'dll_excludes': ['w9xpopen.exe', 'crypt32.dll'],
}


code:
#youtube_dl/extractor/twitter.py: Replace the User Agent with Tor Browser's
#ln=Mozilla/5.0 (Windows NT 6.1; rv:52.0) Gecko/20100101 Firefox/52.0
Mozilla/5.0 (Android 6.0.1; Mobile; rv:54.0) Gecko/54.0 Firefox/54.0


code:
#youtube_dl/extractor/miaopai.py: Replace the User Agent with Tor Browser's
#ln=Mozilla/5.0 (Windows NT 6.1; rv:52.0) Gecko/20100101 Firefox/52.0
self._USER_AGENT_IPAD


code:
#youtube_dl/extractor/youtube.py: Signature decryption not NOT invoking js/swf sig parsers
        if player_type == 'js':
            raise ExtractorError("Youtube's DRM has prevented this software from obtaining the video URL", expected=True)
        elif player_type == 'swf':
            raise ExtractorError("Youtube's DRM has prevented this software from obtaining the video URL", expected=True)


code:
#youtube_dl/extractor/iqiyi.py: Login function, returning an error due to non-free software requirement
    def _login(self):
        raise ExtractorError("iQiyi's non-free authentication algorithm has made login impossible", expected=True)


code:
#devscripts/release.sh: Version determination for this
if [ -z "$1" ]; then echo "ERROR: specify version number like this: $0 1994.09.06"; exit 1; fi
version=`./version.sh`


code:
#Makefile: Wheel, release and executable zipfile creation
	python setup.py sdist bdist_wheel

pub: BINFILES/youtube-dl.deb BINFILES/youtube-dl.tar.gz pypi-files
	mv BINFILES/youtube-dl.tar.gz BINFILES/youtube_dl-`devscripts/version.sh -r`.tar.gz
	mv BINFILES/youtube-dl.deb BINFILES/youtube_dl-`devscripts/version.sh -r`.deb

libu2d: youtube_dl/*.py youtube_dl/*/*.py

youtube-dl.py: libu2d
	./zipEXE.sh
	echo '#!$(PYTHON)' > youtube-dl.py
	echo -n '#' >> youtube-dl.py
	sha256sum youtube-dl.zip >> youtube-dl.py
	cat youtube-dl.zip >> youtube-dl.py
	rm youtube-dl.zip
	chmod a+x youtube-dl.py

README.md: libu2d


code:
#.travis.yml: The right e-mails
  email:
    - gpast_panama@protonmail.com


code:
#Makefile: Point the installer to youtube-dl library, and add library-creater-for-exe-zip-people
youtube_dl.lib:
	echo "import sys" > youtube_dl.lib
	echo "sys.path.insert(0, '`realpath --relative-to $(LIBDIR) $(BINDIR)`/youtube-dl')" >> youtube_dl.lib
	echo "for mod in ('youtube_dl', 'youtu`be_dl'):" >> youtube_dl.lib
	echo "	if mod in sys.modules.keys(): del sys.modules[mod]" >> youtube_dl.lib
	echo "import sys, youtube_dl" >> youtube_dl.lib
	echo "sys.modules['youtu`be_dl'] = sys.modules['youtube_dl']" >> youtube_dl.lib
	echo "sys.path.pop(0)" >> youtube_dl.lib

install: youtube-dl.1 youtube-dl.bash-completion youtube-dl.zsh youtube-dl.fish
	install -d $(DESTDIR)$(BINDIR)
	#Create the 'bootstrap script' for loading from the shell
	echo '#!/usr/bin/env python' > $(DESTDIR)$(BINDIR)/youtube-dl
	echo 'import youtube_dl' >> $(DESTDIR)$(BINDIR)/youtube-dl
	echo "if __name__ == '__main__': youtube_dl.main()" >> $(DESTDIR)$(BINDIR)/youtube-dl
	chmod 755 $(DESTDIR)$(BINDIR)/youtube-dl
	#Install the Python library!
	install -d $(DESTDIR)$(LIBDIR)
	rm -Rf $(DESTDIR)$(LIBDIR)/youtube_dl/
	cp -R youtube_dl/ $(DESTDIR)$(LIBDIR)/youtube_dl/
	find $(DESTDIR)$(LIBDIR)/youtube_dl/ -name "*.pyc" -print | xargs rm
	chmod -R 755 $(DESTDIR)$(LIBDIR)/youtube_dl/
	#Set up symlinks from youtube-dl's equivalent files
	rm -Rf $(DESTDIR)$(BINDIR)/youtu`be-dl $(DESTDIR)$(LIBDIR)/youtu`be-dl
	ln -srv $(DESTDIR)$(BINDIR)/youtube-dl $(DESTDIR)$(BINDIR)/youtu`be-dl
	ln -srv $(DESTDIR)$(LIBDIR)/youtube_dl $(DESTDIR)$(LIBDIR)/youtu`be_dl


code:
#youtube_dl/setup.py: Contact and license details for this
    author='Grace P.',
    author_email='gpast_panama@protonmail.com',
    maintainer='Grace P.',
    maintainer_email='gpast_panama@protonmail.com',


code:
#Makefile: Debian package creation
deb: BINFILES/youtube-dl.deb

BINFILES/youtube-dl.deb: youtube-dl clean
	mkdir -p DebData DebControl
	$(MAKE) install DESTDIR=DebData
	cd DebData && find usr -type f -print0 | xargs -0 md5sum > ../DebControl/md5sums && tar -cJf ../data.tar.xz ./*
	rm -R DebData
	cp DebianPkg/conffiles DebianPkg/p* DebControl/
	env echo -ne "Package: youtube-dl\nVersion: " > DebControl/control
	devscripts/version.sh -r >> DebControl/control
	env echo -ne "\nArchitecture: all\nMaintainer: Grace Past <gpast_panama@protonmail.com>\nInstalled-Size: " >> DebControl/control
	du | tail -1 | awk '{ print $1 }' >> DebControl/control
	cat DebianPkg/control >> DebControl/control
	cd DebControl && tar -czf ../control.tar.gz ./*
	rm -R DebControl
	echo 2.0 > debian-binary
	$(AR) rcs BINFILES/youtube-dl.deb debian-binary control.tar.gz data.tar.xz
	rm debian-binary control.tar.gz data.tar.xz


code:
#Copyright (C) 2017 avideo authors (see AUTHORS)
#
#    This file is part of AVideo.
#
#    AVideo 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 3 of the License, or
#    (at your option) any later version.
#
#    AVideo 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 AVideo.  If not, see <http://www.gnu.org/licenses/>.

import os

#Reader Function- used to read fragments from external files, for the sake of cleanliness
def rd(name, lines=1):
	with open(name, mode="r") as f:
		text = f.read().split("\n", maxsplit=lines)
	return text

files, PATCHES = {}, []
for dirname in ("Patches/Perm/", "Patches/Bug/"):
	if not os.path.exists(dirname):
		continue
	for fileX in sorted(os.listdir(dirname)):
		if fileX.startswith("g"):
			break
		else:
			comment, link, scab = rd(dirname+fileX, 2)
			file, link = comment[1:].split(":", maxsplit=1)[0], link[4:]
			if link == "":
				link = rd(dirname+"g"+fileX)[1]
			else:
				link = link.replace("\\n", "\n").replace("\\t", "\t").replace("\z", "")
			if file in files.keys():
				PATCHES[files[file]].append((scab, link))
			else:
				files[file] = len(PATCHES)
				PATCHES.append([file, (scab, link)])

#The youtube-dl or whatever --> youtu`be-dl or whatever patches in namae.txt

f = open("Patches/namae.txt", mode="r")
segs = f.read().split("\n\n")[1:]
f.close()

for seg in segs:
	seglets = seg.split("\n")
	SAFECODES = [seglets[0][:-1]]
	for seglet in seglets[1:]:
		if seglet != "":
			SAFECODES.append((seglet, seglet.replace("youtube-dl", "youtu`be-dl")))
	PATCHES.append(SAFECODES)


code:
LICENSE
youtube_dl/jsinterp.py
youtube_dl/swfinterp.py
youtube_dl/update.py
test/test_jsinterp.py
test/test_swfinterp.py
devscripts/make_issue_template.py
devscripts/wine-py2exe.sh
test/swftests
test/test_youtube_signature.py
test/test_iqiyi_sdk_interpreter.py
README.md
ChangeLog
devscripts/create-github-release.py
devscripts/show-downloads-statistics.py
devscripts/gh-pages/
test/test_update.py
test/versions.json
.gitignore


code:
##ALSO NEED TO SUBSEQUENTLY ADDRESS URL https://github.com/rg3/youtube-dl/... and http://rg3.github.io/youtube-dl/... and http://yt-dl.org& youtube-dl/ALPHA/NUM
##youtu`be-dl

test/test_all_urls.py:
self.assertMatch('https://www.youtube.com/results?baz=bar&search_query=youtube-dl+test+video&filters=video&lclk=video', ['youtube:search_url'])

youtube_dl/extractor/bandcamp.py:
'title': "youtube-dl  \"'/\\\u00e4\u21ad - youtube-dl test song \"'/\\\u00e4\u21ad",
'url': 'http://youtube-dl.bandcamp.com/track/youtube-dl-test-song',

youtube_dl/extractor/dropbox.py:
'url': 'https://www.dropbox.com/s/nelirfsxnmcfbfh/youtube-dl%20test%20video%20%27%C3%A4%22BaW_jenozKc.mp4?dl=0',
'title': 'youtube-dl test video \'ä"BaW_jenozKc'

youtube_dl/extractor/eighttracks.py:
'url': 'http://8tracks.com/ytdl/youtube-dl-test-tracks-a',
'display_id': 'youtube-dl-test-tracks-a',
'title': "youtube-dl test tracks \"'/\\ä↭<>",
'title': "youtue-dl project<>\"' - youtube-dl test track 1 \"'/\\\u00e4\u21ad",
'title': "youtube-dl project - youtube-dl test track 2 \"'/\\\u00e4\u21ad",
'title': "youtube-dl project as well - youtube-dl test track 3 \"'/\\\u00e4\u21ad",
'title': "youtube-dl project as well - youtube-dl test track 4 \"'/\\\u00e4\u21ad",
'title': "PH - youtube-dl test track 5 \"'/\\\u00e4\u21ad",
'title': "PH - youtube-dl test track 6 \"'/\\\u00e4\u21ad",
'title': "phihag - youtube-dl test track 7 \"'/\\\u00e4\u21ad",
'title': "phihag - youtube-dl test track 8 \"'/\\\u00e4\u21ad",

youtube_dl/extractor/minhateca.py:
'url': 'http://minhateca.com.br/pereba/misc/youtube-dl+test+video,125848331.mp4(video)',
'title': 'youtube-dl test video',

youtube_dl/extractor/moniker.py:
'title': 'youtube-dl test video',

youtube_dl/extractor/sohu.py:
'title': 'youtube-dl testing video',

youtube_dl/extractor/soundcloud.py:
'url': 'https://soundcloud.com/jaimemf/youtube-dl-test-video-a-y-baw/s-8Pjrp',

youtube_dl/extractor/streamcloud.py:
'url': 'http://streamcloud.eu/skp9j99s4bpz/youtube-dl_test_video_____________-BaW_jenozKc.mp4.html',
'title': 'youtube-dl test video  \'/\\ ä ↭',

youtube_dl/extractor/videopremium.py:
'title': 'youtube-dl_test_video____a_________-BaW_jenozKc.mp4.mp4'

youtube_dl/extractor/vidzi.py:
'title': 'youtube-dl test video  1\\\\2\'3/4<5\\\\6ä7↭',

youtube_dl/extractor/vimeo.py:
'videopassword': 'youtube-dl',
'title': "youtube-dl test video - \u2605 \" ' \u5e78 / \\ \u00e4 \u21ad \U0001d550",
'title': 'youtube-dl password protected test video',

youtube_dl/extractor/youtube.py:
'url': 'https://www.youtube.com/results?baz=bar&search_query=youtube-dl+test+video&filters=video&lclk=video',
'title': 'youtube-dl test video',
'title': 'youtube-dl test video "\'/\\ä↭𝕐',
'description': 'test chars:  "\'/\\ä↭𝕐\ntest URL: https://github.com/rg3/youtube-dl/issues/1892\n\nThis is a test video for youtube-dl.\n\nFor more information, contact phihag@phihag.de .',
'tags': ['youtube-dl'],


code:
# AVideo

AVideo /ævə'dɛjo:/ is a powerful, 100% libre video and audio downloader for GNU/Linux. With your freedom and privacy a \#\1 priority, avideo offers you peace of mind in watching everything from world news and documentaries to the latest cat videos.

## Why AVideo?

AVideo builds on the power of the infamous youtube-dl to ensure that the user's liberty is not sacrificed.

YouTube-DL incorporates JS, SWF, and SDK interpreters in order to deliver some functionality. However, packaging these runs contrary to delivering freedom as a number 1 priority.

It was discovered in 2017 on the [Trisquel GNU/Linux fora](https://trisquel.info/en/forum/do-youtube-dlhtml5-video-everywhere-run-nonfree-js) that youtube\-dl included the quite unexpected functionality to run JavaScript.
Further investigation by Grace Past revealed this to be a component of [DRM on YouTube](https://superuser.com/questions/773719/how-do-all-of-these-save-video-from-youtube-services-work#answer-773998), meaning the non-free code sourced from YouTube is an
[unethical means](https://www.gnu.org/philosophy/proprietary.html) to an [unethical end](https://www.defectivebydesign.org/what_is_drm_digital_restrictions_management). Thus, in order to provide you with control over your computer, such a sacrifice unfortunately must be made.

Similar reasoning applies to the other cases of interpreters for non-free software packaged with the parent. If any methods of addressing these issues arise that allow them to be attacked without compromising core values, they shall be promptly implemented;
however, aside from such a possible workaround, it is avideo's aim to avoid compromising [user freedom](https://www.gnu.org/philosophy/free-sw.html).

## Releases
Executable zipfiles and debian packages for current releases, as well as source code, can be found at [the releases page](https://notabug.org/GPast/avideo/wiki/Downloads). Instructions for installation are available in [the User Documentation](https://notabug.org/GPast/avideo/wiki/User+Documentation).
If you already have the software installed, instructions for updating are also available in the user docs.

## Contributing
If you'd like to contribute, we'd enthusiastically welcome you to join us! There's always room for beta testers\- just go to the [Downloads](https://notabug.org/GPast/avideo/wiki/Downloads) page, download and install a beta listing, and give it a spin \(although preferably not in a high-stakes environment...\). If you've got some skills to offer, though, then there might be a good place to put them to work.

For coders, work on [issues](https://notabug.org/GPast/avideo/issues) is appreciated. Adding features or re\-designing components in need of a fix is also a great way to contribute. A list of suggestions can be found in 'TODO.txt', but don't let that limit you! However, before you get your hands into the source, please read the [Developer Documentation](https://notabug.org/GPast/avideo/wiki/Developer+Documentation) to understand what you're dealing with.

For those with linguistic prowess, documentation and/or translation are always in short supply. If you also have some coding skills, making the software more translator\-friendly would be appreciated *immensely*. It appears that avideo is English-only at present, contrary to its mission to bring freedom to everybody.


code:
Things which are which are in the waiting list for completion. Level "1" tasks must be completed before the next release can be made; from then on, larger are less and less urgent.

*Set up the developer's manual [3]
*Edit tests so they consistently give useful information [2]
*Replace placeholder site rebrandings with ones that actually exist [2] 77%
*Finish update system patched- that is, sign release file sigs- and add associated test(s) [1]
*Anonymize request headers for video downloads [3]
*Make the 'latest version' downloads page half-decent [2]
*Increase friendliness to translators- GNU GetText? [2]
*Achieve requirements for GNU compatibility (not necessarily for entry to GNU though) [3]
*Add source-package creation to Makefile [2]
*Create PPA for the software [2]


code:
./YOUTUBE_DL/EXTRACTOR/YOUTUBE.PY:
0: 1
1: 1

./DEVSCRIPTS/RUN_TESTS.SH:
0: 1

./YOUTUBE_DL/EXTRACTOR/IQIYI.PY:
0: 1
1: 1

./DEVSCRIPTS/RELEASE.SH:
0: 1
1: 1

./MAKEFILE:
0: 1
1: 1
2: 1
3: 1
4: 1
5: 1
6: 1
7: 1
8: 1
9: 1
10: 1
11: 1
12: 1

./.TRAVIS.YML:
0: 1

./YOUTUBE_DL/OPTIONS.PY:
0: 1
1: 1

./TEST/TEST_AGE_RESTRICTION.PY:
0: 2

./SETUP.PY:
0: 1
1: 1
2: 1
3: 1

./YOUTUBE_DL/__INIT__.PY:
0: 1
1: 1
2: 1
3: 1

./TEST/TEST_ALL_URLS.PY:
0: 1

./YOUTUBE_DL/EXTRACTOR/BANDCAMP.PY:
0: 1
1: 1

./YOUTUBE_DL/EXTRACTOR/DROPBOX.PY:
0: 1
1: 1

./YOUTUBE_DL/EXTRACTOR/EIGHTTRACKS.PY:
0: 1
1: 1
2: 1
3: 1
4: 1
5: 1
6: 1
7: 1
8: 1
9: 1
10: 1

./YOUTUBE_DL/EXTRACTOR/MINHATECA.PY:
0: 1
1: 1

./YOUTUBE_DL/EXTRACTOR/MONIKER.PY:
0: 3

./YOUTUBE_DL/EXTRACTOR/SOHU.PY:
0: 1

./YOUTUBE_DL/EXTRACTOR/SOUNDCLOUD.PY:
0: 1

./YOUTUBE_DL/EXTRACTOR/STREAMCLOUD.PY:
0: 1
1: 1

./YOUTUBE_DL/EXTRACTOR/VIDEOPREMIUM.PY:
0: 1

./YOUTUBE_DL/EXTRACTOR/VIDZI.PY:
0: 1

./YOUTUBE_DL/EXTRACTOR/VIMEO.PY:
0: 2
1: 1
2: 1

./YOUTUBE_DL/EXTRACTOR/YOUTUBE.PY:
0: 1
1: 1
2: 2
3: 2
4: 2


code:
#!/usr/bin/env python3
#
#Copyright (C) 2017 avideo authors (see AUTHORS)
#
#    This file is part of AVideo.
#
#    AVideo 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 3 of the License, or
#    (at your option) any later version.
#
#    AVideo 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 AVideo.  If not, see <http://www.gnu.org/licenses/>.

import re, os, sys
from Patches import badcode

GPL_HEADER = '''
#
#    This file is part of Foobar.
#
#    Foobar 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 3 of the License, or
#    (at your option) any later version.
#
#    Foobar 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 Foobar.  If not, see <http://www.gnu.org/licenses/>.

'''

def fixer(name, savere):
	gpl_header = GPL_HEADER.replace("Foobar", name)
	with open("./temp.txt", mode="r") as f:
		files = f.read().split("\n")

	for file in files:
		if file == "":
			continue
		newfile = file.replace("youtube-dl", name).replace("youtube_dl", name)
		os.renames(file, newfile)

		#Get file text if this is a text file- if not, we're done with it
		try:
			with open(newfile, mode="r") as f:
				ans = f.read()
		except UnicodeDecodeError:
			continue
		#Don't waste time or add snowflake exceptions...
		if ans == "":
			continue
		#"Protect" github.com/rg3/youtube-dl URLs which point to things that can't be copied (e.g. issue reports),
		#Then convert the URLs as listed at the top of this file, then "unprotect" (i.e. youtu`be --> youtube)
		while True:
			resp = savere.search(ans)
			if resp == None:
				break
			ans = ans.replace(resp.group(0), resp.group(0).replace("youtube-dl", "youtu`be-dl"))
		ans = ans.replace("://github.com/rg3/youtube-dl", "://notabug.org/GPast/%s" % name)
		ans = ans.replace("://rg3.github.io/youtube-dl", "://notabug.org/GPast/%s/wiki" % name)
		ans = ans.replace("://yt-dl.org", "://notabug.org/GPast/%s/wiki" % name)
		ans = ans.replace("youtube-dl", name)
		ans = ans.replace("YOUTUBE-DL", name.upper())
		ans = ans.replace("youtube\-dl", name)
		ans = ans.replace("YOUTUBE\-DL", name.upper())
		ans = ans.replace("youtube_dl", name)
		ans = ans.replace("Youtube-dl", name.capitalize())
		ans = ans.replace("Youtube\-dl", name.capitalize())
		ans = ans.replace("Youtube-DL", name.capitalize())
		ans = ans.replace("Youtube\-DL", name.capitalize())
		ans = ans.replace("youtu`be", "youtube")
		f = open(newfile, mode="w")
		#Also, add GPL spiel at top (below key pseudo-comments!) if this is code
		if newfile.endswith(".py") or newfile.endswith(".sh"):
			while True:
				if ans[0] != "#":
					f.write("\n# Copyright (C) 2017 avideo authors (see AUTHORS)\n")
					f.write(gpl_header)
					f.write(ans)
					break
				seg, ans = ans.split("\n", maxsplit=1)
				f.write(seg+"\n")
		else:
			f.write(ans)
		f.close()

def wipe_code(src):
	ztdout = open("tmp-output", mode="w")

	for bcel in badcode.PATCHES:
		ztdout.write("\n\n./%s:\n" % bcel[0].upper())
		file, dic, n = src+bcel[0], bcel[1:], 0
		with open(file, mode="r") as f:
			edcode = f.read()

		for bc in dic:
			edsegs = edcode.split(bc[0])
			ztdout.write("%d: %d\n" % (n, len(edsegs)-1))
			edcode = bc[1].join(edsegs)
			n += 1

		with open(file, mode="w") as f:
			f.write(edcode)

	ztdout.close()

if __name__ == "__main__":
	wipe_code(sys.argv[1])
	fixer(sys.argv[2], re.compile("://github\\.com/rg3/youtube-dl/[a-z]+/[0-9]+"))


code:
#!/bin/bash -e
#
#Copyright (C) 2017 avideo authors (see AUTHORS)
#
#    This file is part of AVideo.
#
#    AVideo 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 3 of the License, or
#    (at your option) any later version.
#
#    AVideo 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 AVideo.  If not, see <http://www.gnu.org/licenses/>.

function series_new {
	#Create a new series: takes 2 parameters. $1 is this directory's path, and $2 is the date marker.

	if [ -z $2 ]; then
		REL_DATE=`date '+%Y.%-m'`
	else
		echo "Using alternative month data..."
		REL_DATE=$2
	fi

	#Set Up Workspace, Aborting if This Month's Release Already Exists
	if [ ! -z "`git branch | grep ${REL_DATE}`" ]; then
		echo "This Month's Release Already Exists; Aborting."
		exit 3
	fi
	git checkout master
	#Create repository to test for fatal error
	git checkout -b $REL_DATE
	if [ $? != 0 ]; then
		echo "Branch-Creation Error Encountered; Aborting."
		exit 4
	fi
	TZ=UTC date +%s > InitDate.txt
	cd ../Clone
	tar -czf "$1"/youtube-dl.tar.gz --exclude '.git' --exclude '.github' youtube-dl/
	cd "$1"
	git add youtube-dl.tar.gz InitDate.txt
	echo "Committing changes..."
	git commit -S -m "INIT for repos $2."
}

function release_clean {
	#Clean up non-release junk: takes 1 optional parameter. $1 is the software name.
	rm -f temp.txt tmp-output
	rm -Rf "$1"/ youtube-dl/
}

function release_build {
	#Build a release from patches: takes 1 parameter. $1 is the software name.
	release_clean $1
	tar -xzf youtube-dl.tar.gz
	#Copy in patch files, and remove evil files
	cat Patches/ban.txt | while read DEL_FILE
	do
		rm -R youtube-dl/${DEL_FILE}
	done
	cp -R Patches/Files/* youtube-dl/
	#Add some important files
	mv youtube-dl/AUTHORS tempauth
	echo "Youtube-DL Contributors:" > youtube-dl/AUTHORS
	cat tempauth >> youtube-dl/AUTHORS; rm tempauth
	echo -e "\nAVideo Contributors:" >> youtube-dl/AUTHORS
	cat AUTHORS >> youtube-dl/AUTHORS
	#List files in temp.txt and filter out references to interpreters
	find youtube-dl -type f -print > temp.txt
	xargs -a temp.txt sed -i '/swfinterp/d; /jsinterp/d'
	#Set up Git and version info...
	cd youtube-dl/
	devscripts/version.sh > /dev/null
	cd ..
	#Apply code patches and rebrand
	./func.py youtube-dl/ $1
	mkdir $1/BINFILES/
	diff expected_output.txt tmp-output
}

function release_pub {
	#Publish a release: takes 1 required parameter, and 2 optional parameter. $1 is the software name, $2 is --test or -t if tests are to be run, and $3 is --merge or -m if changes are to be merged into the master branch.

	read -p "Is ChangeLog up to date? (y/n) " -n 1 REPLY2
	if [[ ! $REPLY2 =~ ^[Yy]$ ]]; then exit 9; fi

	release_build $1
	version=`./$1/devscripts/version.sh`
	./wiki.py --series-update $version --dlpage $1 --sites-update $1 || ./wiki.py --series-make $version --dlpage $1 --sites-update $1
	cd $1/

	echo "Testing..."
	if [ "$2" == --test ] || [ "$2" == -t ]; then
		echo "Press ^C if you haven't sent through TOR but want to..."
		sleep 5
		nosetests --verbose --with-coverage --cover-package=youtube_dl --cover-html test --stop || exit 1
	else
		echo "Skipping Tests..."
	fi

	SERIES_CURRENT=`echo $version | awk -F . ' { print $1"."$2 } '`
	echo "Building Release Packages..."
	make pub
	#Upload to PyPI
	gpg -ab dist/*.tar.gz
	twine upload dist/*
	#Done Upload
	mv BINFILES/ ../../
	echo "Publishing Files..."
	cd ..
	git add Patches/Files/ChangeLog
	git commit -S -m "Das new ChangeLog" || true
	if [ ${@: -1} == "--merge" ] || [ ${@: -1} == "-m" ]; then
		series_merge
		git push origin master
	fi
	git push origin $SERIES_CURRENT
	git checkout archive
	if [ -d $version ]; then
		echo "Error: $version already exists. Aborting."
		rm -R ../BINFILES
		git checkout `echo $version | awk -F . ' { print $1"."$2 } '`
	fi
	mv ../BINFILES $version

	#Update Repo Files
	cd repos
	cat ../../Wiki/versions.txt | while read lineam
	do
		if [ "$lineam" == "" ]; then
			continue
		fi
		IFS=$'\t' read tag vrs <<< "$lineam"
		tag=`echo $tag | tr -cd [:alpha:]- | tr [:upper:] [:lower:]`
		if [ $(expr length "$tag") == 0 ]; then
			continue
		fi
		mkdir -p ${tag:0:-1}
		cd ${tag:0:-1}
		echo "Origin: notabug.org" > Packages
		echo "Components: main" >> Packages
		echo "Description: avideo official repositories" >> Packages
		echo "Architectures: all" >> Packages
		echo "SignWith: yes" >> Packages
		apt-ftparchive packages ../../$vrs >> Packages
		cat Packages | gzip -9c > Packages.gz
		echo "Origin: notabug.org" > Release
		echo "Architectures: all" >> Release
		echo "Components: main" >> Release
		echo "Description: official avideo "`basename $0`" repository" >> Release
		echo "SignWith: yes" >> Release
		apt-ftparchive release . >> Release
		rm Packages
		gpg --clearsign -o InRelease Release
		gpg -ab -o Release.gpg Release
		git add Packages.gz Release.gpg InRelease Release
		cd ..
	done
	cd ..

	git add $version/
	git commit -S -m "Release $version"
	git push origin archive

	echo "Posting Publication Information..."
	git checkout $SERIES_CURRENT
	cp $1/README.md ../Wiki/"User Documentation.md"
	cd ../Wiki
	git add Downloads.md "Supported Sites.md" "User Documentation.md"
	git commit -S -m "Release $version"
	git push origin master
}

function series_dep {
	#Deprecate a series: takes 2 parameters. $1 is the series to deprecate, and $2 is the software name.
	#UNTESTED!!!
	echo -n "Are you SURE you want to PERMANENTLY delete series ${1}?"
	read -n 1 TEMPANS
	if [ "$TEMPANS" != y ]; then
		exit
	fi
	if (cd ../Wiki; [ ! -z "`git status --porcelain`" ]); then
		echo "Error: workspace is not clean. Please stash or commit changes before continuing."
		exit 4
	fi
	git checkout master
	git branch -D $1
	git push origin :$1

	echo "Posting Deprecation Information..."
	./wiki.py --series-dep $1 --dlpage $2
	cd ../Wiki
	git add DOWNLOADS.md
	git commit $SIGN_COMMIT -m "Release $version"
	git push origin master
}

function series_list {
	#List series: takes 0 parameters.
	git branch | sed '/r/d; s/\*/\ /g'
}

function series_merge {
	#Prepare a specific series branch for (non-destructive) merging into master: takes 0 parameters.
	SERIES_CURRENT=`git rev-parse --abbrev-ref HEAD | tail -1`
	git stash
	git checkout master
	git merge $SERIES_CURRENT || true
	echo "Giving you a bash shell to resolve any merge conflicts; type 'exit' when done."
	bash
	for rem_file in youtube-dl.tar.gz InitDate.txt Patches/Bug; do
		git rm -r $rem_file || true
	done
	git commit -S -m "Merging from ${SERIES_CURRENT}" || true
	git checkout $SERIES_CURRENT
}

function series_giveHelp {
	#Print the help: takes 1 parameter. $1 is the command used to invoke this script.
	echo "Usage: $0 command [date marker]"
	echo "Command is one of:"
	echo "  make-	make a new series."
	echo "  dep-	deprecate a series."
	echo "  clean-	remove temporary files."
	echo "  build-	build a release package."
	echo "	pub-	publish a release."
	echo "  list-	list all series."
	echo "  merge-	merge to master."
	echo "  help-	print this help message."
	echo "A date marker is required for dep, and can be used with make to change the series created; it is ignored elsewhere."
	echo "For pub, two optional parameters are available: use --sign to sign commits, followed by --test to run tests. Omit arguments as relevant."
	exit
}

REL_BIGPATH=`realpath "$0"`
REL_PATH=`dirname "${REL_BIGPATH}"`

if [ $# -eq 0 ]; then
	echo "Error: Insufficient Arguments."
	series_giveHelp "$0"
fi

REL_NAME=avideo
cd "${REL_PATH}"
if [ "$1" == dep ]; then
	if [ $# -lt 2 ]; then
		echo "Error: No Date Marker for 'dep'."
		series_giveHelp "$0"
	fi
	series_dep $2 $REL_NAME
	exit
fi
if [ "$1" == make ]; then
	series_new "$REL_PATH" $2
	exit
fi
if [ "$1" == list ]; then
	series_list
	exit
fi
if [ "$1" == clean ]; then
	release_clean $REL_NAME
	exit
fi
if [ "$1" == build ]; then
	release_build $REL_NAME
	exit
fi
if [ "$1" == pub ]; then
	release_pub $REL_NAME $2 $3
	exit
fi
if [ "$1" == merge ]; then
	series_merge
	exit
fi
echo "Error: Command Not Recognized."
series_giveHelp "$0"


code:
#!/usr/bin/env python3

import sys
WEBSITE = "https://notabug.org/GPast/$NAME/"

def build_dlmod(tag, vrs):
	LINES = ["## $TAG$VRS", "* [Debian Package]("+WEBSITE+"raw/archive/$VRS/$NAME\-$VRS.deb)", \
	"* [Source Code (tar.gz)]("+WEBSITE+"raw/archive/$VRS/$NAME\-$VRS.tar.gz)", ""]
	return "\n\n".join(LINES).replace("$VRS", vrs).replace("$TAG", tag)

def build_dlpage(name):
	with open("../Wiki/versions.txt", mode="r") as f:
		vrs = f.read().splitlines()

	PAGE = "Welcome to the $NAME downloads page! The latest version is **"+vrs[0].split("\t")[1]+\
	"**, and we recommend you use this.\nFor information on installation, please see the [User Documentation]("+\
	WEBSITE+"wiki/User+Documentation).\n\n"
	for line in vrs:
		if line == "":
			continue
		segs = line.split("\t", maxsplit=1)
		PAGE += build_dlmod(*segs)

	PAGE += "## [ALL RELEASES]("+WEBSITE+"src/archive/)"

	with open("../Wiki/Downloads.md", mode="w") as f:
		f.write(PAGE.replace("$NAME", name))
	return 0

def srs_update(vrs):
	with open("../Wiki/versions.txt", mode="r") as f:
		vrsPub = f.read()

	srs = vrs.rsplit(".", maxsplit=1)[0]+"."
	segs = vrsPub.split(srs, maxsplit=1)
	if len(segs) != 2:
		if len(segs) == 1:
			print("Error: series for %s not found in releases." % vrs)
		else:
			print("Error: series for %s appears to be duplicated." % vrs)
		sys.exit(3)
	segsone = segs[1].split("\n", maxsplit=1)

	with open("../Wiki/versions.txt", mode="w") as f:
		f.write(segs[0]+vrs+"\n"+segsone[1])
	return 1

def srs_make(vrs):
	with open("../Wiki/versions.txt", mode="r") as f:
		vrsPub = f.read()
	with open("../Wiki/versions.txt", mode="w") as f:
		f.write("LATEST\\- \t%s\n" % vrs)
		f.write(vrsPub.replace("LATEST\\- ", ""))
	return 1

def srs_dep(srs):
	with open("../Wiki/versions.txt", mode="r") as f:
		vrsPub = f.read().splitlines()

	vrs = ""
	for line in vrsPub:
		if not srs+"." in line:
			vrs += line+"\n"

	with open("../Wiki/versions.txt", mode="w") as f:
		f.write(vrs)
	return 1

def sites_update(name):
	with open(name+"/docs/supportedsites.md", mode="r") as f:
		sites = f.read().splitlines()[1:]
	with open("../Wiki/Supported Sites.md", mode="w") as f:
		f.write("The following sites, listed alphabetically, are supported by the latest release of %s:\n\n" % name)
		f.write("\n".join(sites))

FUNCS = {"dlpage": (build_dlpage, 1), "series-update": (srs_update, 1), "series-make": (srs_make, 1), "series-dep": (srs_dep, 1), "sites-update": (sites_update, 1)}
args = sys.argv[1:]
while len(args) != 0:
	cmd = args.pop(0)[2:]
	cmd, argcnt = FUNCS[cmd]
	argset = args[:argcnt]
	args = args[argcnt:]
	cmd(*argset)


code:
# Default folder where the git repos are located
repos/
forks/
tickets/
docs/
requests/
releases/
remotes/

# Other python/editor specific files to ignore
dist/
build/
*.pyc
*.pyo
*.swp
*.egg-info
*.conf
.coverage

_build/

# Just a local folder I use for testing
clones/

# IdeaIDE
.idea/
# Ignore the git repos in unit tests
tests/*.git

# Don't track alembic.ini
alembic.ini

repo_name: bill/auger

path: .gitignore


code:
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 *


code:
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


code:
Upgrading Pagure
================

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


code:
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()


code:
"""${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"}


code:
"""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')


code:
"""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")


code:
"""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')


code:
"""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')


code:
"""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')


code:
"""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)


code:
"""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)


code:
"""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')


code:
"""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.
    """


code:
"""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')


code:
"""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')


code:
"""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')


code:
"""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')


code:
"""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)
    )


code:
"""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')


code:
"""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')


code:
"""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')


code:
"""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')


code:
"""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')


code:
"""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')


code:
#!/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)


code:
#!/usr/bin/env python
""" Populate the pagure db with some dev data. """
import sys

import os
import pagure
import tests
from sqlalchemy import create_engine, MetaData
import argparse

'''
Usage:
python dev-data.py --init
python dev-data.py --clean
python dev-data.py --populate
python dev-data.py --all
'''

def init_database():
    DB_URL = pagure.APP.config['DB_URL']

    # create the table if it doesnt exist
    pagure.lib.model.create_tables(
        DB_URL,
        pagure.APP.config.get('PATH_ALEMBIC_INI', None),
        acls=pagure.APP.config.get('ACLS', {}),
        debug=True)

    engine = create_engine('%s' % DB_URL, echo=True)

    metadata = MetaData(engine)
    metadata.reflect(bind=engine)
    return engine, metadata

def empty_dev_db(metadata, engine):
    print('')
    print('')
    print('WARNING: Deleting all data from ', pagure.APP.config['DB_URL'])
    # Dangerous: this will wipe the data from the table but keep the schema
    print('')
    response = raw_input('Do you want to continue yes or no?    ')
    if 'yes'.startswith(response.lower()):
        for tbl in reversed(metadata.sorted_tables):
            if tbl.fullname != 'acls':
                engine.execute(tbl.delete())

def insert_data(session, username, user_email):
    pagure.APP.config['EMAIL_SEND'] = False
    pagure.APP.config['TESTING'] = True

    ######################################
    # tags
    item = pagure.lib.model.Tag(
        tag='tag1',
    )
    session.add(item)
    session.commit()

    ######################################
    # Users
    # Create a couple of users
    item = pagure.lib.model.User(
        user='pingou',
        fullname='PY C',
        password='foo',
        default_email='bar@pingou.com',
    )
    session.add(item)
    session.commit()

    item = pagure.lib.model.User(
        user='foo',
        fullname='foo bar',
        password='foo',
        default_email='foo@bar.com',
    )
    session.add(item)
    session.commit()

    item = pagure.lib.model.User(
        user=username,
        fullname=username,
        password='foo',
        default_email=user_email,
    )
    session.add(item)
    session.commit()

    ######################################
    # pagure_group
    item = pagure.lib.model.PagureGroup(
        group_name='admin',
        user_id=1,
    )
    session.add(item)
    session.commit()

    # Add a couple of groups so that we can list them
    item = pagure.lib.model.PagureGroup(
        group_name='group',
        group_type='user',
        user_id=1,  # pingou
    )
    session.add(item)
    session.commit()

    item = pagure.lib.model.PagureGroup(
        group_name='rel-eng',
        group_type='user',
        user_id=1,  # pingou
    )
    session.add(item)
    session.commit()
    ######################################
    # projects

    import shutil
    # delete folder from local instance to start from a clean slate
    if os.path.exists(pagure.APP.config['GIT_FOLDER']):
        shutil.rmtree(pagure.APP.config['GIT_FOLDER'])

    tests.create_projects(session)
    tests.create_projects_git(pagure.APP.config['GIT_FOLDER'], bare=True)
    tests.add_content_git_repo(
        os.path.join(pagure.APP.config['GIT_FOLDER'], 'test.git'))
    tests.add_readme_git_repo(
        os.path.join(pagure.APP.config['GIT_FOLDER'], 'test.git'))

    # Add some content to the git repo
    tests.add_content_git_repo(
        os.path.join(pagure.APP.config['GIT_FOLDER'], 'forks', 'pingou',
                     'test.git'))
    tests.add_readme_git_repo(
        os.path.join(pagure.APP.config['GIT_FOLDER'], 'forks', 'pingou',
                     'test.git'))
    tests.add_commit_git_repo(
        os.path.join(pagure.APP.config['GIT_FOLDER'], 'forks', 'pingou',
                     'test.git'), ncommits=10)

    ######################################
    # user_emails
    item = pagure.lib.model.UserEmail(
        user_id=1,
        email='bar@pingou.com')
    session.add(item)

    item = pagure.lib.model.UserEmail(
        user_id=1,
        email='foo@pingou.com')
    session.add(item)

    item = pagure.lib.model.UserEmail(
        user_id=2,
        email='foo@bar.com')
    session.add(item)

    item = pagure.lib.model.UserEmail(
        user_id=3,
        email=user_email)
    session.add(item)

    session.commit()

    ######################################
    # user_emails_pending
    user = pagure.lib.search_user(session, username='pingou')
    email_pend = pagure.lib.model.UserEmailPending(
        user_id=user.id,
        email='foo@fp.o',
        token='abcdef',
    )
    session.add(email_pend)
    session.commit()

    ######################################
    # issues
    # Add an issue and tag it so that we can list them
    item = pagure.lib.model.Issue(
        id=1,
        uid='foobar',
        project_id=1,
        title='Problem with jenkins build',
        content='For some reason the tests fail at line:24',
        user_id=1,  # pingou
    )
    session.add(item)
    session.commit()

    item = pagure.lib.model.Issue(
        id=2,
        uid='foobar2',
        project_id=1,
        title='Unit tests failing',
        content='Need to fix code for the unit tests to '
                'pass so jenkins build can complete.',
        user_id=1,  # pingou
    )
    session.add(item)
    session.commit()

    user = pagure.lib.search_user(session, username=username)
    item = pagure.lib.model.Issue(
        id=3,
        uid='foobar3',
        project_id=1,
        title='Segfault during execution',
        content='Index out of bounds for variable i?',
        user_id=user.id,  # current user
    )
    session.add(item)
    session.commit()

    ######################################
    # pagure_user_group
    group = pagure.lib.search_groups(session, pattern=None,
                                     group_name="rel-eng", group_type=None)
    user = pagure.lib.search_user(session, username='pingou')
    item = pagure.lib.model.PagureUserGroup(
        user_id=user.id,
        group_id=group.id
    )
    session.add(item)
    session.commit()

    user = pagure.lib.search_user(session, username=username)
    group = pagure.lib.search_groups(session, pattern=None,
                                     group_name="admin", group_type=None)

    item = pagure.lib.model.PagureUserGroup(
        user_id=user.id,
        group_id=group.id
    )
    session.add(item)
    session.commit()

    user = pagure.lib.search_user(session, username='foo')
    group = pagure.lib.search_groups(session, pattern=None,
                                     group_name="group", group_type=None)

    item = pagure.lib.model.PagureUserGroup(
        user_id=user.id,
        group_id=group.id
    )
    session.add(item)
    session.commit()

    ######################################
    # projects_groups
    group = pagure.lib.search_groups(session, pattern=None,
                                     group_name="rel-eng", group_type=None)
    repo = pagure.lib.get_project(session, 'test')
    item = pagure.lib.model.ProjectGroup(
        project_id=repo.id,
        group_id=group.id
    )
    session.add(item)
    session.commit()

    group = pagure.lib.search_groups(session, pattern=None,
                                     group_name="admin", group_type=None)
    repo = pagure.lib.get_project(session, 'test2')
    item = pagure.lib.model.ProjectGroup(
        project_id=repo.id,
        group_id=group.id
    )
    session.add(item)
    session.commit()

    ######################################
    # pull_requests
    repo = pagure.lib.get_project(session, 'test')
    forked_repo = pagure.lib.get_project(session, 'test')
    req = pagure.lib.new_pull_request(
        session=session,
        repo_from=forked_repo,
        branch_from='master',
        repo_to=repo,
        branch_to='master',
        title='Fixing code for unittest',
        user=username,
        requestfolder=None,
    )
    session.commit()

    ######################################
    # tokens
    tests.create_tokens(session)

    ######################################
    # user_projects
    user = pagure.lib.search_user(session, username='foo')
    repo = pagure.lib.get_project(session, 'test')
    item = pagure.lib.model.ProjectUser(
        project_id=repo.id,
        user_id=user.id
    )
    session.add(item)
    session.commit()

    user = pagure.lib.search_user(session, username=username)
    repo = pagure.lib.get_project(session, 'test2')
    item = pagure.lib.model.ProjectUser(
        project_id=repo.id,
        user_id=user.id
    )
    session.add(item)
    session.commit()

    ######################################
    # issue_comments
    item = pagure.lib.model.IssueComment(
        user_id=1,
        issue_uid='foobar',
        comment='We may need to adjust the unittests instead of the code.',
    )
    session.add(item)
    session.commit()

    ######################################
    # issue_to_issue
    repo = pagure.lib.get_project(session, 'test')

    all_issues = pagure.lib.search_issues(session, repo)
    pagure.lib.add_issue_dependency(session, all_issues[0],
                                    all_issues[1], 'pingou',
                                    pagure.APP.config['GIT_FOLDER'])

    ######################################
    # pull_request_comments
    user = pagure.lib.search_user(session, username='pingou')
    # only 1 pull request available atm
    pr = pagure.lib.get_pull_request_of_user(session, "pingou")[0]
    item = pagure.lib.model.PullRequestComment(
        pull_request_uid=pr.uid,
        user_id=user.id,
        comment="+1 for me. Btw, could you rebase before you merge?",
        notification=0
    )
    session.add(item)
    session.commit()

    ######################################
    # pull_request_flags
    user = pagure.lib.search_user(session, username='pingou')
    # only 1 pull request available atm
    pr = pagure.lib.get_pull_request_of_user(session, "pingou")[0]
    item = pagure.lib.model.PullRequestFlag(
        uid="random_pr_flag_uid",
        pull_request_uid=pr.uid,
        user_id=user.id,
        username=user.user,
        percent=80,
        comment="Jenkins build passes",
        url=str(pr.id)
    )
    session.add(item)
    session.commit()

    ######################################
    # tags_issues
    repo = pagure.lib.get_project(session, 'test')
    issues = pagure.lib.search_issues(session, repo)
    item = pagure.lib.model.TagIssue(
        issue_uid=issues[0].uid,
        tag='Blocker',
    )
    session.add(item)
    session.commit()

    ######################################
    # tokens_acls
    tests.create_tokens_acl(session)

    ######################################
    # Fork a project
    # delete fork data
    fork_proj_location = "forks/foo/test.git"
    try:
        shutil.rmtree(os.path.join(pagure.APP.config['GIT_FOLDER'],
                                   fork_proj_location))
    except:
        print('git folder already deleted')

    try:
        shutil.rmtree(os.path.join(pagure.APP.config['DOCS_FOLDER'],
                                   fork_proj_location))
    except:
        print('docs folder already deleted')

    try:
        shutil.rmtree(os.path.join(pagure.APP.config['TICKETS_FOLDER'],
                                   fork_proj_location))
    except:
        print('tickets folder already deleted')

    try:
        shutil.rmtree(os.path.join(pagure.APP.config['REQUESTS_FOLDER'],
                                   fork_proj_location))
    except:
        print('requests folder already deleted')

    repo = pagure.lib.get_project(session, 'test')
    result = pagure.lib.fork_project(session, 'foo', repo,
                                     pagure.APP.config['GIT_FOLDER'],
                                     pagure.APP.config['DOCS_FOLDER'],
                                     pagure.APP.config['TICKETS_FOLDER'],
                                     pagure.APP.config['REQUESTS_FOLDER'])
    if result == 'Repo "test" cloned to "foo/test"':
        session.commit()

if __name__ == "__main__":
    desc = "Run the dev database initialization/insertion/deletion " \
           "script for db located  " + str(pagure.APP.config['DB_URL'])
    parser = argparse.ArgumentParser(prog="dev-data", description=desc)
    parser.add_argument('-i', '--init', action="store_true",
                        help="Create the dev db")
    parser.add_argument('-p', '--populate', action="store_true",
                        help="Add test data to the db")
    parser.add_argument('-d', '--delete', action="store_true",
                        help="Wipe the dev db")
    parser.add_argument('-a', '--all', action="store_true",
                        help="Create, Wipe, Populate the dev db")

    args = parser.parse_args()

    # forcing the user to choose
    if not any(vars(args).values()):
        parser.error('No arguments provided.')

    if args.init or args.delete or args.all:
        eng, meta = init_database()

    if args.delete or args.all:
        empty_dev_db(meta, eng)

    if args.populate or args.all:
        session = pagure.SESSION
        invalid_option = ['pingou', 'bar@pingou.com', 'foo', 'foo@bar.com']
        print("")
        user_name = raw_input(
            "Enter your username so we can add you into the test data:  ")
        while user_name in invalid_option:
            print("Reserved names: " + str(invalid_option))
            user_name = raw_input(
                "Enter your username so we can add you into the test data:  ")

        if not user_name.replace(" ", ""):
            user_name = 'pythagoras'

        print("")
        user_email = raw_input("Enter your user email:  ")

        while user_email in invalid_option:
            print("Reserved names: " + str(invalid_option))
            user_email = raw_input("Enter your user email:  ")

        if not user_email.replace(" ", ""):
            user_email = 'pythagoras@math.com'

        insert_data(session, user_name, user_email)


code:
# 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."

path: doc/Makefile


code:
@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;
}


code:
Pagure API
==========

The API documentation can be found at https://pagure.io/api/0/ or within
the sources in ``pagure/doc/api.rst``.