From bba07e7f6fd4c1b8fff97e4330347d9f3908a9a5 Mon Sep 17 00:00:00 2001
From: Matt Prahl <mprahl@redhat.com>
Date: May 09 2017 20:24:31 +0000
Subject: [PATCH 1/9] Use 'os.path.realpath' in 'get_repo_namespace' to resolve an issue when a symbolic link was being used for APP.config['GIT_FOLDER']


---

diff --git a/pagure/lib/git.py b/pagure/lib/git.py
index 8d4f8ae..90910f3 100644
--- a/pagure/lib/git.py
+++ b/pagure/lib/git.py
@@ -1163,8 +1163,8 @@ def get_repo_namespace(abspath, gitfolder=None):
     if not gitfolder:
         gitfolder = pagure.APP.config['GIT_FOLDER']
 
-    short_path = os.path.abspath(abspath).replace(
-        os.path.abspath(gitfolder), '').strip('/')
+    short_path = os.path.realpath(abspath).replace(
+        os.path.realpath(gitfolder), '').strip('/')
 
     if short_path.startswith('forks/'):
         username, projectname = short_path.split('forks/', 1)[1].split('/', 1)

From 9144f340625ae2c5a9059598c82016e2344d0384 Mon Sep 17 00:00:00 2001
From: Matt Prahl <mprahl@redhat.com>
Date: May 09 2017 20:24:31 +0000
Subject: [PATCH 2/9] Correct variable name in pagure_logcom_server


---

diff --git a/pagure-logcom/pagure_logcom_server.py b/pagure-logcom/pagure_logcom_server.py
index fdb4cb1..e015be0 100644
--- a/pagure-logcom/pagure_logcom_server.py
+++ b/pagure-logcom/pagure_logcom_server.py
@@ -96,7 +96,7 @@ def handle_messages():
         session = pagure.lib.create_session(pagure.APP.config['DB_URL'])
 
         _log.info('Looking for project: %s%s of %s',
-                 '%s/' % namespacerepo if namespace else '',
+                 '%s/' % namespace if namespace else '',
                  repo, username)
         project = pagure.lib._get_project(
             pagure.SESSION, repo, user=username, namespace=namespace)

From 11e5cfdf00984b822939648fca0da841dd4f470f Mon Sep 17 00:00:00 2001
From: Matt Prahl <mprahl@redhat.com>
Date: May 09 2017 20:24:31 +0000
Subject: [PATCH 3/9] Add missing import in pagure_logcom_server


---

diff --git a/pagure-logcom/pagure_logcom_server.py b/pagure-logcom/pagure_logcom_server.py
index e015be0..29bf9d4 100644
--- a/pagure-logcom/pagure_logcom_server.py
+++ b/pagure-logcom/pagure_logcom_server.py
@@ -20,6 +20,7 @@ time can be really time-consuming).
 import json
 import logging
 import os
+from sqlalchemy.exc import SQLAlchemyError
 
 import trollius
 import trollius_redis

From c9a58392409c03c72a228620f2309be55dca3a38 Mon Sep 17 00:00:00 2001
From: Matt Prahl <mprahl@redhat.com>
Date: May 09 2017 20:24:31 +0000
Subject: [PATCH 4/9] Fix imports in pagure_loadjson_server.py


---

diff --git a/pagure-loadjson/pagure_loadjson_server.py b/pagure-loadjson/pagure_loadjson_server.py
index d00f4cd..bfaa362 100644
--- a/pagure-loadjson/pagure_loadjson_server.py
+++ b/pagure-loadjson/pagure_loadjson_server.py
@@ -22,8 +22,7 @@ import json
 import logging
 import os
 import traceback
-
-import requests
+import inspect
 import trollius
 import trollius_redis
 

From 3114c123ea7b5eac4fe42ea13aca1e69948eddd6 Mon Sep 17 00:00:00 2001
From: Matt Prahl <mprahl@redhat.com>
Date: May 09 2017 20:24:31 +0000
Subject: [PATCH 5/9] Add missing decorator for api_git_branches


---

diff --git a/pagure/api/project.py b/pagure/api/project.py
index 9c8fef0..f51684c 100644
--- a/pagure/api/project.py
+++ b/pagure/api/project.py
@@ -70,6 +70,7 @@ def api_git_tags(repo, username=None, namespace=None):
 @API.route('/<namespace>/<repo>/git/branches')
 @API.route('/fork/<username>/<repo>/git/branches')
 @API.route('/fork/<username>/<namespace>/<repo>/git/branches')
+@api_method
 def api_git_branches(repo, username=None, namespace=None):
     '''
     List all the branches of a git repo

From b49f937130f46c238e8341a3a9fbe00e2e38ee07 Mon Sep 17 00:00:00 2001
From: Matt Prahl <mprahl@redhat.com>
Date: May 09 2017 20:24:31 +0000
Subject: [PATCH 6/9] Add different levels of granularity for watching a project and fix flake8 errors for tests to pass


---

diff --git a/alembic/versions/d4d2c5aa8a0_add_granularity_to_watching_repos.py b/alembic/versions/d4d2c5aa8a0_add_granularity_to_watching_repos.py
new file mode 100644
index 0000000..6802807
--- /dev/null
+++ b/alembic/versions/d4d2c5aa8a0_add_granularity_to_watching_repos.py
@@ -0,0 +1,75 @@
+"""Add granularity to watching repos
+
+Revision ID: d4d2c5aa8a0
+Revises: 4255158a6913
+Create Date: 2017-04-28 14:39:09.746953
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = 'd4d2c5aa8a0'
+down_revision = '4255158a6913'
+
+from alembic import op
+import sqlalchemy as sa
+
+# A helper table that is a hybrid with both states. This is used for data
+# migrations later on.
+watcher_helper = sa.Table(
+    'watchers',
+    sa.MetaData(),
+    sa.Column('id', sa.Integer, primary_key=True),
+    sa.Column('watch_issues', sa.Boolean),
+    sa.Column('watch_commits', sa.Boolean),
+    sa.Column('watch', sa.Boolean),
+)
+
+
+def upgrade():
+    op.add_column('watchers', sa.Column('watch_commits', sa.Boolean(),
+                                        nullable=False, server_default='false'))
+    op.add_column('watchers', sa.Column('watch_issues', sa.Boolean(),
+                                        nullable=False, server_default='false'))
+    # This section is to update the `watch_issues` and `watch_commits` columns
+    # with the value of `watch`
+    connection = op.get_bind()
+    for watcher in connection.execute(watcher_helper.select()):
+        connection.execute(
+            watcher_helper.update().where(
+                watcher_helper.c.id == watcher.id
+            ).values(
+                watch_issues=watcher.watch,
+                watch_commits=False
+            )
+        )
+
+    with op.batch_alter_table('watchers') as b:
+        # Remove the server_default now that we've set values
+        b.alter_column('watch_issues', server_default=None)
+        b.alter_column('watch_commits', server_default=None)
+        # Remove the watch column
+        b.drop_column('watch')
+
+
+def downgrade():
+    op.add_column('watchers', sa.Column('watch', sa.BOOLEAN(), nullable=False,
+                                        server_default='false'))
+
+    # This section is to update the `watch` column with the value of
+    # `watch_issues`
+    connection = op.get_bind()
+    for watcher in connection.execute(watcher_helper.select()):
+        connection.execute(
+            watcher_helper.update().where(
+                watcher_helper.c.id == watcher.id
+            ).values(
+                watch=watcher.watch_issues
+            )
+        )
+
+    with op.batch_alter_table('watchers') as b:
+        # Remove the server_default now that we've set values
+        b.alter_column('watch', server_default=None)
+        # Drop the added columns
+        b.drop_column('watch_issues')
+        b.drop_column('watch_commits')
diff --git a/pagure-logcom/pagure_logcom_server.py b/pagure-logcom/pagure_logcom_server.py
index 29bf9d4..bc48382 100644
--- a/pagure-logcom/pagure_logcom_server.py
+++ b/pagure-logcom/pagure_logcom_server.py
@@ -42,7 +42,7 @@ import pagure.lib
 def handle_messages():
     ''' Handles connecting to redis and acting upon messages received.
     In this case, it means logging into the DB the commits specified in the
-    message for the specified repo.
+    message for the default repo or sending commit notification emails.
 
     The currently accepted message format looks like:
 
@@ -62,7 +62,9 @@ def handle_messages():
             "b7b4059c44d692d7df3227ce58ce01191e5407bd",
             "f8d0899bb6654590ffdef66b539fd3b8cf873b35",
             "9b6fdc48d3edab82d3de28953271ea52b0a96117"
-          ]
+          ],
+          "branch": "master",
+          "default_branch": "master"
         }
 
     '''
@@ -89,6 +91,8 @@ def handle_messages():
 
         commits = data['commits']
         abspath = data['abspath']
+        branch = ['data']['branch']
+        default_branch = ['data']['default_branch']
         repo = data['project']['name']
         username = data['project']['username']['name'] \
             if data['project']['parent'] else None
@@ -110,8 +114,14 @@ def handle_messages():
 
         _log.info('Processing %s commits in %s', len(commits), abspath)
 
-        pagure.lib.git.log_commits_to_db(
-            session, project, commits, abspath)
+        # Only log commits when the branch is the default branch
+        if branch == default_branch:
+            pagure.lib.git.log_commits_to_db(
+                session, project, commits, abspath)
+
+        # Notify subscribed users that there are new commits
+        pagure.lib.notify.notify_new_commits(
+            abspath, project, branch, commits)
 
         try:
             session.commit()
diff --git a/pagure/__init__.py b/pagure/__init__.py
index f1ee87d..7f14cb9 100644
--- a/pagure/__init__.py
+++ b/pagure/__init__.py
@@ -426,16 +426,6 @@ def inject_variables():
     if justlogedout:
         flask.session['_justloggedout'] = None
 
-    def is_watching(reponame, username=None, namespace=None):
-        watch = False
-        if authenticated():
-            watch = pagure.lib.is_watching(
-                SESSION, flask.g.fas_user,
-                reponame,
-                repouser=username,
-                namespace=namespace)
-        return watch
-
     new_user = False
     if flask.session.get('_new_user'):
         new_user = True
@@ -446,7 +436,6 @@ def inject_variables():
         admin=user_admin,
         authenticated=authenticated(),
         forkbuttonform=forkbuttonform,
-        is_watching=is_watching,
         new_user=new_user,
     )
 
@@ -502,6 +491,12 @@ def set_variables():
         flask.g.repo_user = is_repo_user(flask.g.repo)
         flask.g.branches = sorted(flask.g.repo_obj.listall_branches())
 
+        repouser = flask.g.repo.user.user if flask.g.repo.is_fork else None
+        fas_user = flask.g.fas_user if authenticated() else None
+        flask.g.repo_watch_levels = pagure.lib.get_watch_level_on_repo(
+            SESSION, fas_user, flask.g.repo.name,
+            repouser=repouser, namespace=namespace)
+
     items_per_page = APP.config['ITEM_PER_PAGE']
     flask.g.offset = 0
     flask.g.page = 1
diff --git a/pagure/hooks/files/default_hook.py b/pagure/hooks/files/default_hook.py
index 790d8d6..ef000b2 100755
--- a/pagure/hooks/files/default_hook.py
+++ b/pagure/hooks/files/default_hook.py
@@ -60,29 +60,37 @@ def run_as_post_receive_hook():
         if not repo_obj.is_empty and not repo_obj.head_is_unborn:
             default_branch = repo_obj.head.shorthand
 
-        # Skip all branch but the default one
-        refname = refname.replace('refs/heads/', '')
-        if refname != default_branch:
-            continue
-
         if set(newrev) == set(['0']):
             print("Deleting a reference/branch, so we won't run the "
                   "pagure hook")
             return
 
+        refname = refname.replace('refs/heads/', '')
         commits = pagure.lib.git.get_revs_between(
             oldrev, newrev, abspath, refname)
 
         if REDIS:
-            print('Sending to redis to log activity')
+            if refname == default_branch:
+                print('Sending to redis to log activity and send commit '
+                      'notification emails')
+            else:
+                print('Sending to redis to send commit notification emails')
+            # If REDIS is enabled, notify subscribed users that there are new
+            # commits to this project
             REDIS.publish(
                 'pagure.logcom',
                 json.dumps({
                     'project': project.to_json(public=True),
                     'abspath': abspath,
+                    'branch': refname,
+                    'default_branch': default_branch,
                     'commits': commits,
                 })
             )
+        else:
+            print('Hook not configured to connect to pagure-logcom')
+            print('/!\ Commit notification emails will not be sent and '
+                  'commits won\'t be logged')
 
     try:
         # Reset the merge_status of all opened PR to refresh their cache
diff --git a/pagure/lib/__init__.py b/pagure/lib/__init__.py
index 4ed0312..0890b19 100644
--- a/pagure/lib/__init__.py
+++ b/pagure/lib/__init__.py
@@ -1982,14 +1982,14 @@ def search_projects(
 
     if not private:
         projects = projects.filter(
-            model.Project.private == False
+            model.Project.private == False  # noqa: E712
         )
     # No filtering is done if private == username i.e  if the owner of the
     # project is viewing the project
     elif isinstance(private, basestring) and private != username:
         projects = projects.filter(
             sqlalchemy.or_(
-                model.Project.private == False,
+                model.Project.private == False,  # noqa: E712
                 sqlalchemy.and_(
                     model.User.user == private,
                     model.User.id == model.ProjectUser.user_id,
@@ -2002,11 +2002,11 @@ def search_projects(
     if fork is not None:
         if fork is True:
             projects = projects.filter(
-                model.Project.is_fork == True
+                model.Project.is_fork == True  # noqa: E712
             )
         elif fork is False:
             projects = projects.filter(
-                model.Project.is_fork == False
+                model.Project.is_fork == False  # noqa: E712
             )
     if tags:
         if not isinstance(tags, (list, tuple)):
@@ -2087,11 +2087,11 @@ def _get_project(session, name, user=None, namespace=None):
         ).filter(
             model.User.id == model.Project.user_id
         ).filter(
-            model.Project.is_fork == True
+            model.Project.is_fork == True  # noqa: E712
         )
     else:
         query = query.filter(
-            model.Project.is_fork == False
+            model.Project.is_fork == False  # noqa: E712
         )
 
     return query.first()
@@ -2286,15 +2286,15 @@ def search_issues(
 
     if private is False:
         query = query.filter(
-            model.Issue.private == False
+            model.Issue.private == False  # noqa: E712
         )
     elif isinstance(private, basestring):
         user2 = aliased(model.User)
         query = query.filter(
             sqlalchemy.or_(
-                model.Issue.private == False,
+                model.Issue.private == False,  # noqa: E712
                 sqlalchemy.and_(
-                    model.Issue.private == True,
+                    model.Issue.private == True,  # noqa: E712
                     model.Issue.user_id == user2.id,
                     user2.user == private,
                 )
@@ -3555,6 +3555,10 @@ def get_pull_request_of_user(session, username):
 def update_watch_status(session, project, user, watch):
     ''' Update the user status for watching a project.
     '''
+    if watch not in ['-1', '0', '1', '2', '3']:
+        raise pagure.exceptions.PagureException(
+            'The watch value of "%s" is invalid' % watch)
+
     user_obj = get_user(session, user)
 
     if not user_obj:
@@ -3578,33 +3582,52 @@ def update_watch_status(session, project, user, watch):
         session.flush()
         return 'Watch status reset'
 
+    should_watch_issues = False
+    should_watch_commits = False
+    if watch == '1':
+        should_watch_issues = True
+    elif watch == '2':
+        should_watch_commits = True
+    elif watch == '3':
+        should_watch_issues = True
+        should_watch_commits = True
+
     if not watcher:
         watcher = model.Watcher(
             project_id=project.id,
             user_id=user_obj.id,
-            watch=watch
+            watch_issues=should_watch_issues,
+            watch_commits=should_watch_commits
         )
     else:
-        watcher.watch = watch
+        watcher.watch_issues = should_watch_issues
+        watcher.watch_commits = should_watch_commits
 
     session.add(watcher)
     session.flush()
 
-    msg_success = 'You are now watching this repo.'
-    if not int(watch):
-        msg_success = 'You are no longer watching this repo.'
-    return msg_success
-
+    if should_watch_issues and should_watch_commits:
+        return 'You are now watching issues, PRs, and commits on this project'
+    elif should_watch_issues:
+        return 'You are now just watching issues and PRs on this project'
+    elif should_watch_commits:
+        return 'You are now just watching commits on this project'
+    else:
+        return 'You are no longer watching this project'
 
-def is_watching(session, user, reponame, repouser=None, namespace=None):
-    ''' Check user watching the project. '''
 
+def get_watch_level_on_repo(session, user, reponame, repouser=None,
+                            namespace=None):
+    ''' Get a list representing the watch level of the user on the project.
+    '''
+    # If a user wasn't passed in, we can't determine their watch level
     if user is None:
-        return False
-
+        return []
+    # If we can't find the user in the database, we can't determine their watch
+    # level
     user_obj = search_user(session, username=user.username)
     if not user_obj:
-        return False
+        return []
 
     query = session.query(
         model.Watcher
@@ -3622,11 +3645,11 @@ def is_watching(session, user, reponame, repouser=None, namespace=None):
         ).filter(
             model.User.id == model.Project.user_id
         ).filter(
-            model.Project.is_fork == True
+            model.Project.is_fork == True  # noqa: E712
         )
     else:
         query = query.filter(
-            model.Project.is_fork == False
+            model.Project.is_fork == False  # noqa: E712
         )
 
     if namespace is not None:
@@ -3635,29 +3658,46 @@ def is_watching(session, user, reponame, repouser=None, namespace=None):
         )
 
     watcher = query.first()
-
+    # If there is a watcher issue, that means the user explicitly set a watch
+    # level on the project
     if watcher:
-        return watcher.watch
+        if watcher.watch_issues and watcher.watch_commits:
+            return ['issues', 'commits']
+        elif watcher.watch_issues:
+            return ['issues']
+        elif watcher.watch_commits:
+            return ['commits']
+        else:
+            # If a watcher entry is set and both are set to False, that means
+            # the user explicitly asked to not be notified
+            return []
 
     project = pagure.get_authorized_project(
         session, reponame, user=repouser, namespace=namespace)
 
+    # If the project is not found, we can't determine the involvement of the
+    # user in the project
     if not project:
-        return False
-
+        return []
+    # If the user is the project owner, by default they will be watching
+    # issues and PRs
     if user.username == project.user.username:
-        return True
-
+        return ['issues']
+    # If the user is a contributor, by default they will be watching issues
+    # and PRs
     for contributor in project.users:
         if user.username == contributor.username:
-            return True
-
+            return ['issues']
+    # If the user is in a project group, by default they will be watching
+    # issues and PRs
     for group in project.groups:
         for guser in group.users:
             if user.username == guser.username:
-                return True
-
-    return False
+                return ['issues']
+    # If no other condition is true, then they are not explicitly watching the
+    # project or are not involved in the project to the point that comes with a
+    # default watch level
+    return []
 
 
 def user_watch_list(session, user, exclude_groups=None):
@@ -3672,7 +3712,9 @@ def user_watch_list(session, user, exclude_groups=None):
     ).filter(
         model.Watcher.user_id == user_obj.id
     ).filter(
-        model.Watcher.watch == False
+        model.Watcher.watch_issues == False  # noqa: E712
+    ).filter(
+        model.Watcher.watch_commits == False  # noqa: E712
     )
 
     unwatched_list = []
@@ -3684,7 +3726,9 @@ def user_watch_list(session, user, exclude_groups=None):
     ).filter(
         model.Watcher.user_id == user_obj.id
     ).filter(
-        model.Watcher.watch == True
+        model.Watcher.watch_issues == True  # noqa: E712
+    ).filter(
+        model.Watcher.watch_commits == True  # noqa: E712
     )
 
     watched_list = []
@@ -3810,7 +3854,7 @@ def get_watch_list(session, obj):
     if not private:
         # Add all the people watching the repo, remove those who opted-out
         for watcher in project_watchers_query.all():
-            if watcher.watch:
+            if watcher.watch_issues:
                 users.add(watcher.user.username)
             else:
                 if watcher.user.username in users:
diff --git a/pagure/lib/git.py b/pagure/lib/git.py
index 90910f3..ccb1f25 100644
--- a/pagure/lib/git.py
+++ b/pagure/lib/git.py
@@ -1148,6 +1148,14 @@ def get_author_email(commit, abspath):
     return user
 
 
+def get_commit_subject(commit, abspath):
+    ''' Return the subject of the commit. '''
+    subject = pagure.lib.git.read_git_lines(
+        ['log', '-1', '--pretty=format:"%s"', commit],
+        abspath)[0].replace('"', '')
+    return subject
+
+
 def get_repo_name(abspath):
     ''' Return the name of the git repo based on its path.
     '''
diff --git a/pagure/lib/model.py b/pagure/lib/model.py
index d7b1e1a..011a6e3 100644
--- a/pagure/lib/model.py
+++ b/pagure/lib/model.py
@@ -442,12 +442,6 @@ class Project(BASE):
         viewonly=True
     )
 
-    unwatchers = relation(
-        "Watcher",
-        primaryjoin="and_(Project.id==Watcher.project_id, "
-        "Watcher.watch=='0')"
-    )
-
     @property
     def isa(self):
         ''' A string to allow finding out that this is a project. '''
@@ -647,7 +641,7 @@ class Project(BASE):
         ).filter(
             Issue.status == 'Open'
         ).filter(
-            Issue.private == False
+            Issue.private == False  # noqa: E712
         ).count()
 
     @property
@@ -2055,9 +2049,8 @@ class Watcher(BASE):
         sa.ForeignKey('users.id', onupdate='CASCADE'),
         nullable=False,
         index=True)
-    watch = sa.Column(
-        sa.Boolean,
-        nullable=False)
+    watch_issues = sa.Column(sa.Boolean, nullable=False, default=False)
+    watch_commits = sa.Column(sa.Boolean, nullable=False, default=False)
 
     user = relation(
         'User', foreign_keys=[user_id], remote_side=[User.id],
diff --git a/pagure/lib/notify.py b/pagure/lib/notify.py
index c6a2777..6435ed3 100644
--- a/pagure/lib/notify.py
+++ b/pagure/lib/notify.py
@@ -135,14 +135,6 @@ def _get_emails_for_obj(obj):
     if obj.assignee and obj.assignee.default_email:
         emails.add(obj.assignee.default_email)
 
-    # Add the person watching this project, if the issue is public
-    if obj.isa == 'issue' and not obj.private:
-        for watcher in obj.project.watchers:
-            emails.add(watcher.user.default_email)
-    elif obj.isa == 'pull-request':
-        for watcher in obj.project.watchers:
-            emails.add(watcher.user.default_email)
-
     # Add public notifications to lists/users set project-wide
     if obj.isa == 'issue' and not obj.private:
         for notifs in obj.project.notifications.get('issues', []):
@@ -151,10 +143,17 @@ def _get_emails_for_obj(obj):
         for notifs in obj.project.notifications.get('requests', []):
             emails.add(notifs)
 
-    # Remove the person list in unwatch
-    for unwatcher in obj.project.unwatchers:
-        if unwatcher.user.default_email in emails:
-            emails.remove(unwatcher.user.default_email)
+    # Add the person watching this project, if it's a public issue or a
+    # pull-request
+    if (obj.isa == 'issue' and not obj.private) or obj.isa == 'pull-request':
+        for watcher in obj.project.watchers:
+            if watcher.watch_issues:
+                emails.add(watcher.user.default_email)
+            else:
+                # If there is a watch entry and it is false, it means the user
+                # explicitly requested to not watch the issue
+                if watcher.user.default_email in emails:
+                    emails.remove(watcher.user.default_email)
 
     # Add/Remove people who explicitly asked to be added/removed
     for watcher in obj.watchers:
@@ -172,6 +171,21 @@ def _get_emails_for_obj(obj):
     return emails
 
 
+def _get_emails_for_commit_notification(project):
+    emails = set()
+    for watcher in project.watchers:
+        if watcher.watch_commits:
+            emails.add(watcher.user.default_email)
+
+    # Drop the email used by pagure when sending
+    emails = _clean_emails(
+        emails, pagure.APP.config.get(pagure.APP.config.get(
+            'FROM_EMAIL', 'pagure@fedoraproject.org'))
+    )
+
+    return emails
+
+
 def _build_url(*args):
     ''' Build a URL from a given list of arguments. '''
     items = []
@@ -678,3 +692,48 @@ Your pagure admin.
         email.email,
         user_from=user.fullname or user.user,
     )
+
+
+def notify_new_commits(abspath, project, branch, commits):
+    ''' Notify the people following a project's commits that new commits have
+    been added.
+    '''
+    commits_info = []
+    for commit in commits:
+        commits_info.append({
+            'commit': commit,
+            'author': pagure.lib.git.get_author(commit, abspath),
+            'subject': pagure.lib.git.get_commit_subject(commit, abspath)
+        })
+
+    commits_string = '\n'.join('{0}    {1}    {2}'.format(
+        commit_info['commit'], commit_info['author'], commit_info['subject'])
+        for commit_info in commits_info)
+    commit_url = _build_url(
+        pagure.APP.config['APP_URL'], _fullname_to_url(project.fullname),
+        'commits', branch)
+
+    email_body = '''
+The following commits were pushed to the repo "{repo}" on branch
+"{branch}", which you are following:
+{commits}
+
+
+
+To view more about the commits, visit:
+{commit_url}
+'''
+    email_body = email_body.format(
+        repo=project.fullname,
+        branch=branch,
+        commits=commits_string,
+        commit_url=commit_url
+    )
+    mail_to = _get_emails_for_commit_notification(project)
+
+    send_email(
+        email_body,
+        'New Commits To "{0}" ({1})'.format(project.fullname, branch),
+        ','.join(mail_to),
+        project_name=project.fullname
+    )
diff --git a/pagure/templates/repo_master.html b/pagure/templates/repo_master.html
index 734dc0b..25c68fe 100644
--- a/pagure/templates/repo_master.html
+++ b/pagure/templates/repo_master.html
@@ -3,7 +3,19 @@
 {% block title %}{{
     repo.namespace + '/' if repo.namespace }}{{ repo.name }}{% endblock %}
 {% set tag = "home" %}
-
+{% if g.repo_watch_levels == ['issues', 'commits'] %}
+    {% set watch_button_title = 'You are watching issues, PRs, and commits on this project' %}
+    {% set watch_button_class = 'btn-primary' %}
+{% elif g.repo_watch_levels == ['issues'] %}
+    {% set watch_button_title = 'You are watching issues on this project' %}
+    {% set watch_button_class = 'btn-info' %}
+{% elif g.repo_watch_levels == ['commits'] %}
+    {% set watch_button_title = 'You are watching commits on this project' %}
+    {% set watch_button_class = 'btn-info' %}
+{% else %}
+    {% set watch_button_title = 'You are not watching this project' %}
+    {% set watch_button_class = 'btn-secondary' %}
+{% endif %}
 
 {% block content %}
 <div class="repo-header p-t-1">
@@ -62,24 +74,9 @@
               <span class="oi" data-glyph="fork"></span>Fork</button>
             {% endif %}
           {% endif %}
-            <button title="{%-
-            if is_watching(
-              repo.name,
-              repo.user.user if repo.is_fork else None,
-              repo.namespace
-            )%}You are watching this project{%
-            else %}You are not watching this project{%
-            endif -%}"
-            type="button" class="btn btn-sm dropdown-toggle
-            {%-
-            if is_watching(
-              repo.name,
-              repo.user.user if repo.is_fork else None,
-              repo.namespace
-            )%} btn-primary{%
-            else %} btn-secondary{%
-            endif -%}"
-            data-toggle="dropdown" id="watch-button">
+            <button title="{{ watch_button_title }}" type="button"
+                    class="btn btn-sm dropdown-toggle {{ watch_button_class }}"
+                    data-toggle="dropdown" id="watch-button">
               <span class="oi" data-glyph="eye"></span>
             </button>
             <div class="dropdown-menu dropdown-menu-right watch-menu">
@@ -89,19 +86,21 @@
                   user=repo.user.user if repo.is_fork else None,
                   namespace=repo.namespace,
                   watch=0) }}">
-               {%- if is_watching(
-                  repo.name,
-                  repo.user.user if repo.is_fork else None,
-                  repo.namespace)
-               %}
-               <a class="dropdown-item" id="unwatch_button">Unwatch {{
-                  repo.name }}</a>
-               {% else %}
-               <a class="dropdown-item" id="watch_button">Watch {{
-                  repo.name }}</a>
-               {% endif -%}
-               <a class="dropdown-item" id="reset_button">Reset Watch Status {{
-                  repo.name }}</a>
+                <a class="dropdown-item" id="watch_issues_button">
+                    Watch issues and PRs on "{{ repo.name  }}"
+                </a>
+                <a class="dropdown-item" id="watch_commits_button">
+                    Watch commits on "{{ repo.name }}"
+                </a>
+                <a class="dropdown-item" id="watch_issues_commits_button">
+                    Watch issues, PRs, and commits on "{{ repo.name }}"
+                </a>
+                <a class="dropdown-item" id="unwatch_button">
+                    Unwatch "{{ repo.name }}"
+                </a>
+                <a class="dropdown-item" id="reset_button">
+                    Reset watch status on "{{ repo.name }}"
+                </a>
                 {{ forkbuttonform.csrf_token }}
               </form>
             </div>
@@ -307,20 +306,32 @@
 {{ super() }}
 <script type="text/javascript">
 $(document).ready(function() {
-  $(".watch-menu a").click(function(){
-    var selectedValue = $(this).attr('id');
-    var action = $("#watch_project").attr('action');
-      if (selectedValue === "watch_button") {
-        action = action.replace('/settings/0', '/settings/1');
-        $('#watch_project').attr('action', action);
-      } else if (selectedValue === "reset_button") {
-        action = action.replace('/settings/0', '/settings/-1');
-        $('#watch_project').attr('action', action);
-      }
+    {% if g.repo_watch_levels %}
+        var currentWatchStatusButton = $('#watch_{{ g.repo_watch_levels | join('_') }}_button');
+    {% else %}
+        var currentWatchStatusButton = $('#unwatch_button');
+    {% endif %}
+    currentWatchStatusButton.prepend('<span class="oi" data-glyph="circle-check" style="padding-right:0.5em"></span>');
+    $('.watch-menu a').not(currentWatchStatusButton).css('padding-left', '2.85em');
+
+    $(".watch-menu a").click(function(){
+        var selectedValue = $(this).attr('id');
+        var watchProjectForm = $("#watch_project");
+        var action = watchProjectForm.attr('action');
 
-      $('#watch_project').submit();
+        if (selectedValue === "watch_issues_button") {
+            action = action.replace('/settings/0', '/settings/1');
+        } else if (selectedValue === "watch_commits_button") {
+            action = action.replace('/settings/0', '/settings/2');
+        } else if (selectedValue === "watch_issues_commits_button") {
+            action = action.replace('/settings/0', '/settings/3');
+        } else if (selectedValue === "reset_button") {
+            action = action.replace('/settings/0', '/settings/-1');
+        }
 
-  });
+        watchProjectForm.attr('action', action);
+        watchProjectForm.submit();
+    });
 });
 </script>
 {% endblock %}
diff --git a/pagure/ui/repo.py b/pagure/ui/repo.py
index 422b4a8..5e4acd5 100644
--- a/pagure/ui/repo.py
+++ b/pagure/ui/repo.py
@@ -2261,7 +2261,7 @@ def view_project_activity(repo, namespace=None):
     methods=['POST'])
 @login_required
 def watch_repo(repo, watch, username=None, namespace=None):
-    """ Marked for watching or Unwatching
+    """ Marked for watching or unwatching
     """
 
     return_point = flask.url_for('index')
@@ -2272,7 +2272,7 @@ def watch_repo(repo, watch, username=None, namespace=None):
     if not form.validate_on_submit():
         flask.abort(400)
 
-    if str(watch) not in ['0', '1', '-1']:
+    if str(watch) not in ['0', '1', '2', '3', '-1']:
         flask.abort(400)
 
     try:
diff --git a/tests/test_pagure_flask_ui_repo.py b/tests/test_pagure_flask_ui_repo.py
index 642b227..5c92b83 100644
--- a/tests/test_pagure_flask_ui_repo.py
+++ b/tests/test_pagure_flask_ui_repo.py
@@ -4123,20 +4123,33 @@ index 0000000..fb7093d
             self.assertEqual(output.status_code, 404)
 
             output = self.app.post(
-                '/test/watch/settings/2', data=data, follow_redirects=True)
+                '/test/watch/settings/8', data=data, follow_redirects=True)
             self.assertEqual(output.status_code, 400)
 
             output = self.app.post(
                 '/test/watch/settings/0', data=data, follow_redirects=True)
             self.assertIn(
                 '</button>\n                      You are no longer'
-                ' watching this repo.', output.data)
+                ' watching this project', output.data)
 
             output = self.app.post(
                 '/test/watch/settings/1', data=data, follow_redirects=True)
             self.assertIn(
                 '</button>\n                      You are now'
-                ' watching this repo.', output.data)
+                ' just watching issues and PRs on this project', output.data)
+
+            output = self.app.post(
+                '/test/watch/settings/2', data=data, follow_redirects=True)
+            self.assertIn(
+                '</button>\n                      You are now'
+                ' just watching commits on this project', output.data)
+
+            output = self.app.post(
+                '/test/watch/settings/3', data=data, follow_redirects=True)
+            self.assertIn(
+                ('</button>\n                      You are now'
+                 ' watching issues, PRs, and commits on this project'),
+                output.data)
 
             item = pagure.lib.model.Project(
                 user_id=2,  # foo
@@ -4163,14 +4176,29 @@ index 0000000..fb7093d
                 follow_redirects=True)
             self.assertIn(
                 '</button>\n                      You are no longer'
-                ' watching this repo.', output.data)
+                ' watching this project', output.data)
 
             output = self.app.post(
                 '/fork/foo/test/watch/settings/1', data=data,
                 follow_redirects=True)
             self.assertIn(
                 '</button>\n                      You are now'
-                ' watching this repo.', output.data)
+                ' just watching issues and PRs on this project', output.data)
+
+            output = self.app.post(
+                '/fork/foo/test/watch/settings/2', data=data,
+                follow_redirects=True)
+            self.assertIn(
+                '</button>\n                      You are now'
+                ' just watching commits on this project', output.data)
+
+            output = self.app.post(
+                '/fork/foo/test/watch/settings/3', data=data,
+                follow_redirects=True)
+            self.assertIn(
+                ('</button>\n                      You are now'
+                 ' watching issues, PRs, and commits on this project'),
+                output.data)
 
             output = self.app.post(
                 '/fork/foo/test/watch/settings/-1', data=data,
@@ -4179,7 +4207,6 @@ index 0000000..fb7093d
                 '</button>\n                      Watch status reset',
                 output.data)
 
-
     def test_delete_report(self):
         """ Test the  delete_report endpoint. """
 
diff --git a/tests/test_pagure_lib.py b/tests/test_pagure_lib.py
index a3cd9ff..58e6363 100644
--- a/tests/test_pagure_lib.py
+++ b/tests/test_pagure_lib.py
@@ -3143,10 +3143,20 @@ class PagureLibtests(tests.Modeltests):
             session=self.session,
             project=project,
             user='aavrug',
-            watch=True,
+            watch='1',
         )
 
-        # All good and when user seleted reset watch option.
+        # Invalid watch status
+        self.assertRaises(
+            pagure.exceptions.PagureException,
+            pagure.lib.update_watch_status,
+            session=self.session,
+            project=project,
+            user='pingou',
+            watch='me fail',
+        )
+
+        # All good and when user selected reset watch option.
         msg = pagure.lib.update_watch_status(
             session=self.session,
             project=project,
@@ -3156,25 +3166,26 @@ class PagureLibtests(tests.Modeltests):
         self.session.commit()
         self.assertEqual(msg, 'Watch status is already reset')
 
-        # All good and when user seleted watch option.
+        # All good and when user selected watch issues option.
         msg = pagure.lib.update_watch_status(
             session=self.session,
             project=project,
             user='pingou',
-            watch=True,
+            watch='1',
         )
         self.session.commit()
-        self.assertEqual(msg, 'You are now watching this repo.')
+        self.assertEqual(
+            msg, 'You are now just watching issues and PRs on this project')
 
         # All good and when user selected unwatch option.
         msg = pagure.lib.update_watch_status(
             session=self.session,
             project=project,
             user='pingou',
-            watch=False,
+            watch='0',
         )
         self.session.commit()
-        self.assertEqual(msg, 'You are no longer watching this repo.')
+        self.assertEqual(msg, 'You are no longer watching this project')
 
         # All good and when user seleted reset watch option.
         msg = pagure.lib.update_watch_status(
@@ -3186,30 +3197,30 @@ class PagureLibtests(tests.Modeltests):
         self.session.commit()
         self.assertEqual(msg, 'Watch status reset')
 
-    def test_is_watching(self):
-        """ Test the is_watching method of pagure.lib. """
+    def test_get_watch_level_on_repo(self):
+        """ Test the get_watch_level_on_repo method of pagure.lib. """
         tests.create_projects(self.session)
         self.test_add_group()
 
         project = pagure.lib._get_project(self.session, 'test')
 
         # If user not logged in
-        watch = pagure.lib.is_watching(
+        watch_level = pagure.lib.get_watch_level_on_repo(
             session=self.session,
             user=None,
             reponame='test',
         )
-        self.assertFalse(watch)
+        self.assertEqual(watch_level, [])
 
         # User does not exist
         user = tests.FakeUser()
         user.username = 'aavrug'
-        watch = pagure.lib.is_watching(
+        watch_level = pagure.lib.get_watch_level_on_repo(
             session=self.session,
             user=user,
             reponame='test',
         )
-        self.assertFalse(watch)
+        self.assertEqual(watch_level, [])
 
         pagure.lib.add_group_to_project(
             session=self.session,
@@ -3232,56 +3243,97 @@ class PagureLibtests(tests.Modeltests):
 
         # If user belongs to any group of that project
         user.username = 'foo'
-        watch = pagure.lib.is_watching(
+        msg = watch_level = pagure.lib.get_watch_level_on_repo(
             session=self.session,
             user=user,
             reponame='test',
         )
-        self.assertTrue(watch)
+        self.assertEqual(watch_level, ['issues'])
 
         # If user is the creator
         user.username = 'pingou'
-        watch = pagure.lib.is_watching(
+        watch_level = pagure.lib.get_watch_level_on_repo(
             session=self.session,
             user=user,
             reponame='test',
         )
-        self.assertTrue(watch)
+        self.assertEqual(watch_level, ['issues'])
 
-        # Entry into watchers table
-        pagure.lib.update_watch_status(
+        # Entry into watchers table for issues and commits
+        msg = pagure.lib.update_watch_status(
             session=self.session,
             project=project,
             user='pingou',
-            watch=True,
+            watch='3',
         )
         self.session.commit()
+        self.assertEqual(
+            msg,
+            'You are now watching issues, PRs, and commits on this project')
+
+        # From watchers table
+        watch_level = pagure.lib.get_watch_level_on_repo(
+            session=self.session,
+            user=user,
+            reponame='test',
+        )
+        self.assertEqual(['issues', 'commits'], watch_level)
+
+        # Entry into watchers table for just commits
+        msg = pagure.lib.update_watch_status(
+            session=self.session,
+            project=project,
+            user='pingou',
+            watch='2',
+        )
+        self.session.commit()
+        self.assertEqual(
+            msg, 'You are now just watching commits on this project')
+
+        # From watchers table
+        watch_level = pagure.lib.get_watch_level_on_repo(
+            session=self.session,
+            user=user,
+            reponame='test',
+        )
+        self.assertEqual(['commits'], watch_level)
+
+        # Entry into watchers table for issues
+        msg = pagure.lib.update_watch_status(
+            session=self.session,
+            project=project,
+            user='pingou',
+            watch='1',
+        )
+        self.session.commit()
+        self.assertEqual(
+            msg, 'You are now just watching issues and PRs on this project')
 
         # From watchers table
-        watch = pagure.lib.is_watching(
+        watch_level = pagure.lib.get_watch_level_on_repo(
             session=self.session,
             user=user,
             reponame='test',
         )
-        self.assertTrue(watch)
+        self.assertEqual(['issues'], watch_level)
 
-        # Entry into watchers table
+        # Entry into watchers table for no watching
         msg = pagure.lib.update_watch_status(
             session=self.session,
             project=project,
             user='pingou',
-            watch=False,
+            watch='0',
         )
         self.session.commit()
-        self.assertEqual(msg, 'You are no longer watching this repo.')
+        self.assertEqual(msg, 'You are no longer watching this project')
 
         # From watchers table
-        watch = pagure.lib.is_watching(
+        watch_level = pagure.lib.get_watch_level_on_repo(
             session=self.session,
             user=user,
             reponame='test',
         )
-        self.assertFalse(watch)
+        self.assertEqual(watch_level, [])
 
         # Add a contributor to the project
         item = pagure.lib.model.User(
@@ -3306,12 +3358,12 @@ class PagureLibtests(tests.Modeltests):
 
         # Check if the new contributor is watching
         user.username = 'bar'
-        watch = pagure.lib.is_watching(
+        watch_level = pagure.lib.get_watch_level_on_repo(
             session=self.session,
             user=user,
             reponame='test',
         )
-        self.assertTrue(watch)
+        self.assertEqual(watch_level, ['issues'])
 
     def test_user_watch_list(self):
         ''' test user watch list method of pagure.lib '''
diff --git a/tests/test_pagure_lib_notify.py b/tests/test_pagure_lib_notify.py
index 1f6e891..2c4074e 100644
--- a/tests/test_pagure_lib_notify.py
+++ b/tests/test_pagure_lib_notify.py
@@ -162,8 +162,9 @@ class PagureLibNotifytests(tests.Modeltests):
 
         # Watch the project
         repo = pagure.get_authorized_project(self.session, 'test3', namespace='ns')
-        out = pagure.lib.update_watch_status(self.session, repo, 'bar', True)
-        self.assertEqual(out, 'You are now watching this repo.')
+        out = pagure.lib.update_watch_status(self.session, repo, 'bar', '1')
+        self.assertEqual(
+            out, 'You are now just watching issues and PRs on this project')
 
         exp = set(['bar@pingou.com', 'foo@bar.com', 'bar@bar.com'])
         out = pagure.lib.notify._get_emails_for_obj(iss)
@@ -323,8 +324,9 @@ class PagureLibNotifytests(tests.Modeltests):
 
         # Watch the project
         repo = pagure.get_authorized_project(self.session, 'test')
-        out = pagure.lib.update_watch_status(self.session, repo, 'bar', True)
-        self.assertEqual(out, 'You are now watching this repo.')
+        out = pagure.lib.update_watch_status(self.session, repo, 'bar', '1')
+        self.assertEqual(
+            out, 'You are now just watching issues and PRs on this project')
 
         exp = set(['bar@pingou.com', 'foo@bar.com', 'bar@bar.com'])
         out = pagure.lib.notify._get_emails_for_obj(req)
diff --git a/tests/test_pagure_lib_watch_list.py b/tests/test_pagure_lib_watch_list.py
index c2b075d..cb0d5ed 100644
--- a/tests/test_pagure_lib_watch_list.py
+++ b/tests/test_pagure_lib_watch_list.py
@@ -309,10 +309,10 @@ class PagureLibGetWatchListtests(tests.Modeltests):
             session=self.session,
             project=project,
             user='pingou',
-            watch=False,
+            watch='0',
         )
         self.session.commit()
-        self.assertEqual(msg, 'You are no longer watching this repo.')
+        self.assertEqual(msg, 'You are no longer watching this project')
 
         # Create the ticket
         iss = pagure.lib.new_issue(
@@ -432,10 +432,11 @@ class PagureLibGetWatchListtests(tests.Modeltests):
             session=self.session,
             project=project,
             user='bar',
-            watch=True,
+            watch='1',
         )
         self.session.commit()
-        self.assertEqual(msg, 'You are now watching this repo.')
+        self.assertEqual(
+            msg, 'You are now just watching issues and PRs on this project')
 
         # Create the pull-request
         req = pagure.lib.new_pull_request(
@@ -492,10 +493,11 @@ class PagureLibGetWatchListtests(tests.Modeltests):
             session=self.session,
             project=project,
             user='bar',
-            watch=True,
+            watch='1',
         )
         self.session.commit()
-        self.assertEqual(msg, 'You are now watching this repo.')
+        self.assertEqual(
+            msg, 'You are now just watching issues and PRs on this project')
 
         # Create the ticket
         iss = pagure.lib.new_issue(

From a47c0d60af8f6f7ad3718cba5ecad5ff681e9d2f Mon Sep 17 00:00:00 2001
From: Matt Prahl <mprahl@redhat.com>
Date: May 09 2017 20:24:31 +0000
Subject: [PATCH 7/9] Remove server_default in the add_granularity_to_watching_repos migration


---

diff --git a/alembic/versions/d4d2c5aa8a0_add_granularity_to_watching_repos.py b/alembic/versions/d4d2c5aa8a0_add_granularity_to_watching_repos.py
index 6802807..fca2de2 100644
--- a/alembic/versions/d4d2c5aa8a0_add_granularity_to_watching_repos.py
+++ b/alembic/versions/d4d2c5aa8a0_add_granularity_to_watching_repos.py
@@ -27,9 +27,9 @@ watcher_helper = sa.Table(
 
 def upgrade():
     op.add_column('watchers', sa.Column('watch_commits', sa.Boolean(),
-                                        nullable=False, server_default='false'))
+                                        nullable=True))
     op.add_column('watchers', sa.Column('watch_issues', sa.Boolean(),
-                                        nullable=False, server_default='false'))
+                                        nullable=True))
     # This section is to update the `watch_issues` and `watch_commits` columns
     # with the value of `watch`
     connection = op.get_bind()
@@ -44,16 +44,15 @@ def upgrade():
         )
 
     with op.batch_alter_table('watchers') as b:
-        # Remove the server_default now that we've set values
-        b.alter_column('watch_issues', server_default=None)
-        b.alter_column('watch_commits', server_default=None)
+        # Set nullable to False now that we've set values
+        b.alter_column('watch_issues', nullable=False)
+        b.alter_column('watch_commits', nullable=False)
         # Remove the watch column
         b.drop_column('watch')
 
 
 def downgrade():
-    op.add_column('watchers', sa.Column('watch', sa.BOOLEAN(), nullable=False,
-                                        server_default='false'))
+    op.add_column('watchers', sa.Column('watch', sa.BOOLEAN(), nullable=True))
 
     # This section is to update the `watch` column with the value of
     # `watch_issues`
@@ -68,8 +67,8 @@ def downgrade():
         )
 
     with op.batch_alter_table('watchers') as b:
-        # Remove the server_default now that we've set values
-        b.alter_column('watch', server_default=None)
+        # Set nullable to False now that we've set values
+        b.alter_column('watch', nullable=False)
         # Drop the added columns
         b.drop_column('watch_issues')
         b.drop_column('watch_commits')

From 733eaa20e051fb53943a69d4595c256bdfc712ec Mon Sep 17 00:00:00 2001
From: Matt Prahl <mprahl@redhat.com>
Date: May 09 2017 20:24:31 +0000
Subject: [PATCH 8/9] Add a project watchers API endpoint


---

diff --git a/pagure/api/project.py b/pagure/api/project.py
index f51684c..1839828 100644
--- a/pagure/api/project.py
+++ b/pagure/api/project.py
@@ -66,6 +66,83 @@ def api_git_tags(repo, username=None, namespace=None):
     return jsonout
 
 
+@API.route('/<repo>/watchers')
+@API.route('/<namespace>/<repo>/watchers')
+@API.route('/fork/<username>/<repo>/watchers')
+@API.route('/fork/<username>/<namespace>/<repo>/watchers')
+@api_method
+def api_project_watchers(repo, username=None, namespace=None):
+    '''
+    Project watchers
+    ----------------
+    List the watchers on the project.
+
+    ::
+
+        GET /api/0/<repo>/watchers
+        GET /api/0/<namespace>/<repo>/watchers
+
+    ::
+
+        GET /api/0/fork/<username>/<repo>/watchers
+        GET /api/0/fork/<username>/<namespace>/<repo>/watchers
+
+    Sample response
+    ^^^^^^^^^^^^^^^
+
+    ::
+
+        {
+            "total_watchers": 1,
+            "watchers": {
+                "mprahl": [
+                    "issues",
+                    "commits"
+                ]
+            }
+        }
+    '''
+    repo = pagure.get_authorized_project(
+        SESSION, repo, user=username, namespace=namespace)
+    if repo is None:
+        raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT)
+
+    implicit_watch_users = {repo.user.username}
+    for access_type in repo.access_users.keys():
+        implicit_watch_users = \
+            implicit_watch_users | set(
+                [user.username for user in repo.access_users[access_type]])
+    for access_type in repo.access_groups.keys():
+        group_names = [group.group_name
+                       for group in repo.access_groups[access_type]]
+        for group_name in group_names:
+            group = pagure.lib.search_groups(SESSION, group_name=group_name)
+            implicit_watch_users = \
+                implicit_watch_users | set([
+                    user.username for user in group.users])
+
+    watching_users_to_watch_level = {}
+    for implicit_watch_user in implicit_watch_users:
+        user_watch_level = pagure.lib.get_watch_level_on_repo(
+            SESSION, implicit_watch_user, repo)
+        watching_users_to_watch_level[implicit_watch_user] = user_watch_level
+
+    # Get the explicit watch statuses
+    for watcher in repo.watchers:
+        if watcher.watch_issues or watcher.watch_commits:
+            watching_users_to_watch_level[watcher.user.username] = \
+                pagure.lib.get_watch_level_on_repo(
+                    SESSION, watcher.user.username, repo)
+        else:
+            if watcher.user.username in watching_users_to_watch_level:
+                watching_users_to_watch_level.pop(watcher.user.username, None)
+
+    return flask.jsonify({
+        'total_watchers': len(watching_users_to_watch_level),
+        'watchers': watching_users_to_watch_level
+    })
+
+
 @API.route('/<repo>/git/branches')
 @API.route('/<namespace>/<repo>/git/branches')
 @API.route('/fork/<username>/<repo>/git/branches')
diff --git a/pagure/lib/__init__.py b/pagure/lib/__init__.py
index 0890b19..bfcb038 100644
--- a/pagure/lib/__init__.py
+++ b/pagure/lib/__init__.py
@@ -3616,47 +3616,47 @@ def update_watch_status(session, project, user, watch):
         return 'You are no longer watching this project'
 
 
-def get_watch_level_on_repo(session, user, reponame, repouser=None,
+def get_watch_level_on_repo(session, user, repo, repouser=None,
                             namespace=None):
     ''' Get a list representing the watch level of the user on the project.
     '''
     # If a user wasn't passed in, we can't determine their watch level
     if user is None:
         return []
+    elif isinstance(user, six.string_types):
+        user_obj = search_user(session, username=user)
+    else:
+        user_obj = search_user(session, username=user.username)
     # If we can't find the user in the database, we can't determine their watch
     # level
-    user_obj = search_user(session, username=user.username)
     if not user_obj:
         return []
 
+    # If the user passed in a Project for the repo parameter, then we don't
+    # need to query for it
+    if isinstance(repo, model.Project):
+        project = repo
+    # If the user passed in a string, then assume it is a project name
+    elif isinstance(repo, six.string_types):
+        project = pagure.get_authorized_project(
+            session, repo, user=repouser, namespace=namespace)
+    else:
+        raise RuntimeError('The passed in repo is an invalid type of "{0}"'
+                           .format(type(repo).__name__))
+
+    # If the project is not found, we can't determine the involvement of the
+    # user in the project
+    if not project:
+        return []
+
     query = session.query(
         model.Watcher
     ).filter(
         model.Watcher.user_id == user_obj.id
     ).filter(
         model.Watcher.project_id == model.Project.id
-    ).filter(
-        model.Project.name == reponame
     )
 
-    if repouser is not None:
-        query = query.filter(
-            model.User.user == repouser
-        ).filter(
-            model.User.id == model.Project.user_id
-        ).filter(
-            model.Project.is_fork == True  # noqa: E712
-        )
-    else:
-        query = query.filter(
-            model.Project.is_fork == False  # noqa: E712
-        )
-
-    if namespace is not None:
-        query = query.filter(
-            model.Project.namespace == namespace
-        )
-
     watcher = query.first()
     # If there is a watcher issue, that means the user explicitly set a watch
     # level on the project
@@ -3672,27 +3672,20 @@ def get_watch_level_on_repo(session, user, reponame, repouser=None,
             # the user explicitly asked to not be notified
             return []
 
-    project = pagure.get_authorized_project(
-        session, reponame, user=repouser, namespace=namespace)
-
-    # If the project is not found, we can't determine the involvement of the
-    # user in the project
-    if not project:
-        return []
     # If the user is the project owner, by default they will be watching
     # issues and PRs
-    if user.username == project.user.username:
+    if user_obj.username == project.user.username:
         return ['issues']
     # If the user is a contributor, by default they will be watching issues
     # and PRs
     for contributor in project.users:
-        if user.username == contributor.username:
+        if user_obj.username == contributor.username:
             return ['issues']
     # If the user is in a project group, by default they will be watching
     # issues and PRs
     for group in project.groups:
         for guser in group.users:
-            if user.username == guser.username:
+            if user_obj.username == guser.username:
                 return ['issues']
     # If no other condition is true, then they are not explicitly watching the
     # project or are not involved in the project to the point that comes with a
diff --git a/tests/test_pagure_flask_api_project.py b/tests/test_pagure_flask_api_project.py
index 46e5b40..baa179d 100644
--- a/tests/test_pagure_flask_api_project.py
+++ b/tests/test_pagure_flask_api_project.py
@@ -532,6 +532,194 @@ class PagureFlaskApiProjecttests(tests.Modeltests):
         }
         self.assertDictEqual(data, expected_data)
 
+    def test_api_project_watchers(self):
+        """ Test the api_project_watchers method of the flask api. """
+        tests.create_projects(self.session)
+        # The user is not logged in and the owner is watching issues implicitly
+        output = self.app.get('/api/0/test/watchers')
+        self.assertEqual(output.status_code, 200)
+        expected_data = {
+            "total_watchers": 1,
+            "watchers": {
+                "pingou": [
+                    "issues"
+                ]
+            }
+        }
+        self.assertDictEqual(json.loads(output.data), expected_data)
+
+        user = pagure.SESSION.query(pagure.lib.model.User).filter_by(
+            user='pingou')
+        with tests.user_set(pagure.APP, user):
+            # Non-existing project
+            output = self.app.get('/api/0/random/watchers')
+            self.assertEqual(output.status_code, 404)
+            data = json.loads(output.data)
+            self.assertDictEqual(
+                data,
+                {'error_code': 'ENOPROJECT', 'error': 'Project not found'}
+            )
+
+            # The owner is watching issues implicitly
+            output = self.app.get('/api/0/test/watchers')
+            self.assertEqual(output.status_code, 200)
+            expected_data = {
+                "total_watchers": 1,
+                "watchers": {
+                    "pingou": [
+                        "issues"
+                    ]
+                }
+            }
+            self.assertDictEqual(json.loads(output.data), expected_data)
+
+            project = pagure.get_authorized_project(self.session, 'test')
+
+            # The owner is watching issues and commits explicitly
+            pagure.lib.update_watch_status(
+                pagure.SESSION, project, 'pingou', '3')
+            output = self.app.get('/api/0/test/watchers')
+            self.assertEqual(output.status_code, 200)
+            expected_data = {
+                "total_watchers": 1,
+                "watchers": {
+                    "pingou": [
+                        "issues",
+                        "commits"
+                    ]
+                }
+            }
+            self.assertDictEqual(json.loads(output.data), expected_data)
+
+            # The owner is watching issues explicitly
+            pagure.lib.update_watch_status(
+                pagure.SESSION, project, 'pingou', '1')
+            output = self.app.get('/api/0/test/watchers')
+            self.assertEqual(output.status_code, 200)
+            expected_data = {
+                "total_watchers": 1,
+                "watchers": {
+                    "pingou": [
+                        "issues"
+                    ]
+                }
+            }
+            self.assertDictEqual(json.loads(output.data), expected_data)
+
+            # The owner is watching commits explicitly
+            pagure.lib.update_watch_status(
+                pagure.SESSION, project, 'pingou', '2')
+            output = self.app.get('/api/0/test/watchers')
+            self.assertEqual(output.status_code, 200)
+            expected_data = {
+                "total_watchers": 1,
+                "watchers": {
+                    "pingou": [
+                        "commits"
+                    ]
+                }
+            }
+            self.assertDictEqual(json.loads(output.data), expected_data)
+
+            # The owner is watching commits explicitly and foo is watching
+            # issues implicitly
+            project_user = pagure.lib.model.ProjectUser(
+                project_id=project.id,
+                user_id=2,
+                access='commit',
+            )
+            pagure.lib.update_watch_status(
+                pagure.SESSION, project, 'pingou', '2')
+            pagure.SESSION.add(project_user)
+            pagure.SESSION.commit()
+
+            output = self.app.get('/api/0/test/watchers')
+            self.assertEqual(output.status_code, 200)
+            expected_data = {
+                "total_watchers": 2,
+                "watchers": {
+                    "foo": ["issues"],
+                    "pingou": ["commits"]
+                }
+            }
+            self.assertDictEqual(json.loads(output.data), expected_data)
+
+            # The owner and foo are watching issues implicitly
+            pagure.lib.update_watch_status(
+                pagure.SESSION, project, 'pingou', '-1')
+
+            output = self.app.get('/api/0/test/watchers')
+            self.assertEqual(output.status_code, 200)
+            expected_data = {
+                "total_watchers": 2,
+                "watchers": {
+                    "foo": ["issues"],
+                    "pingou": ["issues"]
+                }
+            }
+            self.assertDictEqual(json.loads(output.data), expected_data)
+
+            # The owner and foo through group membership are watching issues
+            # implicitly
+            pagure.lib.update_watch_status(
+                pagure.SESSION, project, 'pingou', '-1')
+            project_membership = pagure.SESSION.query(
+                pagure.lib.model.ProjectUser).filter_by(
+                    user_id=2, project_id=project.id).one()
+            pagure.SESSION.delete(project_membership)
+            pagure.SESSION.commit()
+            msg = pagure.lib.add_group(
+                self.session,
+                group_name='some_group',
+                display_name='Some Group',
+                description=None,
+                group_type='bar',
+                user='pingou',
+                is_admin=False,
+                blacklist=[],
+            )
+            pagure.SESSION.commit()
+            group = pagure.SESSION.query(pagure.lib.model.PagureGroup)\
+                .filter_by(group_name='some_group').one()
+            pagure.lib.add_user_to_group(
+                pagure.SESSION, 'foo', group, 'pingou', False)
+            project_group = pagure.lib.model.ProjectGroup(
+                project_id=project.id,
+                group_id=group.id,
+                access='commit',
+            )
+            pagure.SESSION.add(project_group)
+            pagure.SESSION.commit()
+
+            output = self.app.get('/api/0/test/watchers')
+            self.assertEqual(output.status_code, 200)
+            expected_data = {
+                "total_watchers": 2,
+                "watchers": {
+                    "foo": ["issues"],
+                    "pingou": ["issues"]
+                }
+            }
+            self.assertDictEqual(json.loads(output.data), expected_data)
+
+            # The owner is watching issues implicitly and foo will be watching
+            # commits explicitly but is in a group with commit access
+            pagure.lib.update_watch_status(
+                pagure.SESSION, project, 'pingou', '-1')
+            pagure.lib.update_watch_status(
+                pagure.SESSION, project, 'foo', '2')
+
+            output = self.app.get('/api/0/test/watchers')
+            self.assertEqual(output.status_code, 200)
+            expected_data = {
+                "total_watchers": 2,
+                "watchers": {
+                    "foo": ["commits"],
+                    "pingou": ["issues"]
+                }
+            }
+            self.assertDictEqual(json.loads(output.data), expected_data)
+
     @patch('pagure.lib.git.generate_gitolite_acls')
     def test_api_new_project(self, p_gga):
         """ Test the api_new_project method of the flask api. """
diff --git a/tests/test_pagure_lib.py b/tests/test_pagure_lib.py
index 58e6363..a0d648c 100644
--- a/tests/test_pagure_lib.py
+++ b/tests/test_pagure_lib.py
@@ -3208,7 +3208,7 @@ class PagureLibtests(tests.Modeltests):
         watch_level = pagure.lib.get_watch_level_on_repo(
             session=self.session,
             user=None,
-            reponame='test',
+            repo='test',
         )
         self.assertEqual(watch_level, [])
 
@@ -3218,7 +3218,7 @@ class PagureLibtests(tests.Modeltests):
         watch_level = pagure.lib.get_watch_level_on_repo(
             session=self.session,
             user=user,
-            reponame='test',
+            repo='test',
         )
         self.assertEqual(watch_level, [])
 
@@ -3246,7 +3246,7 @@ class PagureLibtests(tests.Modeltests):
         msg = watch_level = pagure.lib.get_watch_level_on_repo(
             session=self.session,
             user=user,
-            reponame='test',
+            repo='test',
         )
         self.assertEqual(watch_level, ['issues'])
 
@@ -3255,7 +3255,7 @@ class PagureLibtests(tests.Modeltests):
         watch_level = pagure.lib.get_watch_level_on_repo(
             session=self.session,
             user=user,
-            reponame='test',
+            repo='test',
         )
         self.assertEqual(watch_level, ['issues'])
 
@@ -3275,7 +3275,7 @@ class PagureLibtests(tests.Modeltests):
         watch_level = pagure.lib.get_watch_level_on_repo(
             session=self.session,
             user=user,
-            reponame='test',
+            repo='test',
         )
         self.assertEqual(['issues', 'commits'], watch_level)
 
@@ -3294,7 +3294,7 @@ class PagureLibtests(tests.Modeltests):
         watch_level = pagure.lib.get_watch_level_on_repo(
             session=self.session,
             user=user,
-            reponame='test',
+            repo='test',
         )
         self.assertEqual(['commits'], watch_level)
 
@@ -3313,7 +3313,7 @@ class PagureLibtests(tests.Modeltests):
         watch_level = pagure.lib.get_watch_level_on_repo(
             session=self.session,
             user=user,
-            reponame='test',
+            repo='test',
         )
         self.assertEqual(['issues'], watch_level)
 
@@ -3331,7 +3331,7 @@ class PagureLibtests(tests.Modeltests):
         watch_level = pagure.lib.get_watch_level_on_repo(
             session=self.session,
             user=user,
-            reponame='test',
+            repo='test',
         )
         self.assertEqual(watch_level, [])
 
@@ -3361,7 +3361,7 @@ class PagureLibtests(tests.Modeltests):
         watch_level = pagure.lib.get_watch_level_on_repo(
             session=self.session,
             user=user,
-            reponame='test',
+            repo='test',
         )
         self.assertEqual(watch_level, ['issues'])
 

From d7ac6648359ad36ec5b53bd4d67e528f8bf46c72 Mon Sep 17 00:00:00 2001
From: Pierre-Yves Chibon <pingou@pingoured.fr>
Date: May 12 2017 20:13:37 +0000
Subject: [PATCH 9/9] Small wording adjustment


Signed-off-by: Pierre-Yves Chibon <pingou@pingoured.fr>

---

diff --git a/pagure/lib/__init__.py b/pagure/lib/__init__.py
index bfcb038..5f52795 100644
--- a/pagure/lib/__init__.py
+++ b/pagure/lib/__init__.py
@@ -3609,9 +3609,9 @@ def update_watch_status(session, project, user, watch):
     if should_watch_issues and should_watch_commits:
         return 'You are now watching issues, PRs, and commits on this project'
     elif should_watch_issues:
-        return 'You are now just watching issues and PRs on this project'
+        return 'You are now watching issues and PRs on this project'
     elif should_watch_commits:
-        return 'You are now just watching commits on this project'
+        return 'You are now watching commits on this project'
     else:
         return 'You are no longer watching this project'
 
diff --git a/tests/test_pagure_flask_ui_repo.py b/tests/test_pagure_flask_ui_repo.py
index 5c92b83..34d14ee 100644
--- a/tests/test_pagure_flask_ui_repo.py
+++ b/tests/test_pagure_flask_ui_repo.py
@@ -4136,13 +4136,13 @@ index 0000000..fb7093d
                 '/test/watch/settings/1', data=data, follow_redirects=True)
             self.assertIn(
                 '</button>\n                      You are now'
-                ' just watching issues and PRs on this project', output.data)
+                ' watching issues and PRs on this project', output.data)
 
             output = self.app.post(
                 '/test/watch/settings/2', data=data, follow_redirects=True)
             self.assertIn(
                 '</button>\n                      You are now'
-                ' just watching commits on this project', output.data)
+                ' watching commits on this project', output.data)
 
             output = self.app.post(
                 '/test/watch/settings/3', data=data, follow_redirects=True)
@@ -4183,14 +4183,14 @@ index 0000000..fb7093d
                 follow_redirects=True)
             self.assertIn(
                 '</button>\n                      You are now'
-                ' just watching issues and PRs on this project', output.data)
+                ' watching issues and PRs on this project', output.data)
 
             output = self.app.post(
                 '/fork/foo/test/watch/settings/2', data=data,
                 follow_redirects=True)
             self.assertIn(
                 '</button>\n                      You are now'
-                ' just watching commits on this project', output.data)
+                ' watching commits on this project', output.data)
 
             output = self.app.post(
                 '/fork/foo/test/watch/settings/3', data=data,
diff --git a/tests/test_pagure_lib.py b/tests/test_pagure_lib.py
index a0d648c..3d93f52 100644
--- a/tests/test_pagure_lib.py
+++ b/tests/test_pagure_lib.py
@@ -3175,7 +3175,7 @@ class PagureLibtests(tests.Modeltests):
         )
         self.session.commit()
         self.assertEqual(
-            msg, 'You are now just watching issues and PRs on this project')
+            msg, 'You are now watching issues and PRs on this project')
 
         # All good and when user selected unwatch option.
         msg = pagure.lib.update_watch_status(
@@ -3288,7 +3288,7 @@ class PagureLibtests(tests.Modeltests):
         )
         self.session.commit()
         self.assertEqual(
-            msg, 'You are now just watching commits on this project')
+            msg, 'You are now watching commits on this project')
 
         # From watchers table
         watch_level = pagure.lib.get_watch_level_on_repo(
@@ -3307,7 +3307,7 @@ class PagureLibtests(tests.Modeltests):
         )
         self.session.commit()
         self.assertEqual(
-            msg, 'You are now just watching issues and PRs on this project')
+            msg, 'You are now watching issues and PRs on this project')
 
         # From watchers table
         watch_level = pagure.lib.get_watch_level_on_repo(
diff --git a/tests/test_pagure_lib_notify.py b/tests/test_pagure_lib_notify.py
index 2c4074e..9463f84 100644
--- a/tests/test_pagure_lib_notify.py
+++ b/tests/test_pagure_lib_notify.py
@@ -164,7 +164,7 @@ class PagureLibNotifytests(tests.Modeltests):
         repo = pagure.get_authorized_project(self.session, 'test3', namespace='ns')
         out = pagure.lib.update_watch_status(self.session, repo, 'bar', '1')
         self.assertEqual(
-            out, 'You are now just watching issues and PRs on this project')
+            out, 'You are now watching issues and PRs on this project')
 
         exp = set(['bar@pingou.com', 'foo@bar.com', 'bar@bar.com'])
         out = pagure.lib.notify._get_emails_for_obj(iss)
@@ -326,7 +326,7 @@ class PagureLibNotifytests(tests.Modeltests):
         repo = pagure.get_authorized_project(self.session, 'test')
         out = pagure.lib.update_watch_status(self.session, repo, 'bar', '1')
         self.assertEqual(
-            out, 'You are now just watching issues and PRs on this project')
+            out, 'You are now watching issues and PRs on this project')
 
         exp = set(['bar@pingou.com', 'foo@bar.com', 'bar@bar.com'])
         out = pagure.lib.notify._get_emails_for_obj(req)
diff --git a/tests/test_pagure_lib_watch_list.py b/tests/test_pagure_lib_watch_list.py
index cb0d5ed..0cf88df 100644
--- a/tests/test_pagure_lib_watch_list.py
+++ b/tests/test_pagure_lib_watch_list.py
@@ -436,7 +436,7 @@ class PagureLibGetWatchListtests(tests.Modeltests):
         )
         self.session.commit()
         self.assertEqual(
-            msg, 'You are now just watching issues and PRs on this project')
+            msg, 'You are now watching issues and PRs on this project')
 
         # Create the pull-request
         req = pagure.lib.new_pull_request(
@@ -497,7 +497,7 @@ class PagureLibGetWatchListtests(tests.Modeltests):
         )
         self.session.commit()
         self.assertEqual(
-            msg, 'You are now just watching issues and PRs on this project')
+            msg, 'You are now watching issues and PRs on this project')
 
         # Create the ticket
         iss = pagure.lib.new_issue(

