webhooks: Support filtering GitHub activity from private repositories.

Currently, the GitHub webhook sends activity from both public and private
repositories, which could lead to unintended disclosure of sensitive
information from private repositories.

This commit introduces a ignore_private_repositories parameter to the
webhook URL. When set to true, the webhook ignore processing activity from
private repositories, ensuring that such activities are not posted to
Zulip streams. By default, if the parameter is omitted or set to false,
activities from both public and private repositories are processed
normally. This provides users with the flexibility to control the
visibility of private repository activities without altering the default
behavior.

More importantly, this introduces a cleaner mechanism for individual
incoming webhooks to declare support for settings not common to all
webhook integrations.

Fixes #31638.
This commit is contained in:
Aditya Kumar Kasaudhan 2024-09-20 15:11:30 +05:30 committed by Tim Abbott
parent fdf90f7ad1
commit d1ff871523
20 changed files with 927 additions and 12 deletions

View File

@ -20,6 +20,13 @@ format used by the Zulip server that they are interacting with.
## Changes in Zulip 10.0 ## Changes in Zulip 10.0
**Feature level 318**
* [`POST /register`](/api/register-queue): Updated
`realm_incoming_webhook_bots` with a new `config_options` key,
defining which options should be offered when creating URLs for this
integration.
**Feature level 317** **Feature level 317**
* [`POST /user_groups/create`](/api/create-user-group): * [`POST /user_groups/create`](/api/create-user-group):

View File

@ -34,7 +34,7 @@ DESKTOP_WARNING_VERSION = "5.9.3"
# new level means in api_docs/changelog.md, as well as "**Changes**" # new level means in api_docs/changelog.md, as well as "**Changes**"
# entries in the endpoint's documentation in `zulip.yaml`. # entries in the endpoint's documentation in `zulip.yaml`.
API_FEATURE_LEVEL = 317 # Last bumped for inclusion of `group_id` in user group creation response. API_FEATURE_LEVEL = 318 # Last bumped for `WebhookConfigOption` configuration update in realm_incoming_webhook_bots.
# Bump the minor PROVISION_VERSION to indicate that folks should provision # Bump the minor PROVISION_VERSION to indicate that folks should provision
# only when going from an old version of the code to a newer version. Bump # only when going from an old version of the code to a newer version. Bump

View File

@ -1,7 +1,10 @@
import ClipboardJS from "clipboard"; import ClipboardJS from "clipboard";
import $ from "jquery"; import $ from "jquery";
import type * as tippy from "tippy.js"; import type * as tippy from "tippy.js";
import {z} from "zod";
import render_generate_integration_url_config_checkbox_modal from "../templates/settings/generate_integration_url_config_checkbox_modal.hbs";
import render_generate_integration_url_config_text_modal from "../templates/settings/generate_integration_url_config_text_modal.hbs";
import render_generate_integration_url_modal from "../templates/settings/generate_integration_url_modal.hbs"; import render_generate_integration_url_modal from "../templates/settings/generate_integration_url_modal.hbs";
import render_integration_events from "../templates/settings/integration_events.hbs"; import render_integration_events from "../templates/settings/integration_events.hbs";
@ -14,6 +17,20 @@ import {realm} from "./state_data";
import * as stream_data from "./stream_data"; import * as stream_data from "./stream_data";
import * as util from "./util"; import * as util from "./util";
type ConfigOption = {
key: string;
label: string;
validator: string;
};
const config_option_schema = z.object({
key: z.string(),
label: z.string(),
validator: z.string(),
});
const config_options_schema = z.array(config_option_schema);
export function show_generate_integration_url_modal(api_key: string): void { export function show_generate_integration_url_modal(api_key: string): void {
const default_url_message = $t_html({defaultMessage: "Integration URL will appear here."}); const default_url_message = $t_html({defaultMessage: "Integration URL will appear here."});
const streams = stream_data.subscribed_subs(); const streams = stream_data.subscribed_subs();
@ -42,6 +59,7 @@ export function show_generate_integration_url_modal(api_key: string): void {
const $integration_url = $("#generate-integration-url-modal .integration-url"); const $integration_url = $("#generate-integration-url-modal .integration-url");
const $dialog_submit_button = $("#generate-integration-url-modal .dialog_submit_button"); const $dialog_submit_button = $("#generate-integration-url-modal .dialog_submit_button");
const $show_integration_events = $("#show-integration-events"); const $show_integration_events = $("#show-integration-events");
const $config_container = $("#integration-url-config-options-container");
$dialog_submit_button.prop("disabled", true); $dialog_submit_button.prop("disabled", true);
$("#integration-url-stream_widget").prop("disabled", true); $("#integration-url-stream_widget").prop("disabled", true);
@ -57,6 +75,40 @@ export function show_generate_integration_url_modal(api_key: string): void {
); );
}); });
function render_config(config: ConfigOption[]): void {
const validated_config = config_options_schema.parse(config);
$config_container.empty();
for (const option of validated_config) {
let $config_element: JQuery;
if (option.validator === "check_bool") {
const config_html = render_generate_integration_url_config_checkbox_modal({
key: option.key,
label: option.label,
});
$config_element = $(config_html);
$config_element
.find(`#integration-url-${option.key}-checkbox`)
.on("change", () => {
update_url();
});
} else if (option.validator === "check_string") {
const config_html = render_generate_integration_url_config_text_modal({
key: option.key,
label: option.label,
});
$config_element = $(config_html);
$config_element.find(`#integration-url-${option.key}-text`).on("change", () => {
update_url();
});
} else {
continue;
}
$config_container.append($config_element);
}
}
$override_topic.on("change", function () { $override_topic.on("change", function () {
const checked = this.checked; const checked = this.checked;
$topic_input.parent().toggleClass("hide", !checked); $topic_input.parent().toggleClass("hide", !checked);
@ -109,6 +161,7 @@ export function show_generate_integration_url_modal(api_key: string): void {
(bot) => bot.name === selected_integration, (bot) => bot.name === selected_integration,
); );
const all_event_types = selected_integration_data?.all_event_types; const all_event_types = selected_integration_data?.all_event_types;
const config = selected_integration_data?.config_options;
if (all_event_types !== null) { if (all_event_types !== null) {
$("#integration-events-parameter").removeClass("hide"); $("#integration-events-parameter").removeClass("hide");
@ -136,8 +189,27 @@ export function show_generate_integration_url_modal(api_key: string): void {
params.set("topic", topic_name); params.set("topic", topic_name);
} }
} }
const selected_events = set_events_param(params); const selected_events = set_events_param(params);
if (config) {
for (const option of config) {
let $input_element;
if (option.validator === "check_bool") {
$input_element = $(`#integration-url-${option.key}-checkbox`);
if ($input_element.prop("checked")) {
params.set(option.key, "true");
}
} else if (option.validator === "check_string") {
$input_element = $(`#integration-url-${option.key}-text`);
const value = $input_element.val();
if (value) {
params.set(option.key, value.toString());
}
}
}
}
const realm_url = realm.realm_url; const realm_url = realm.realm_url;
const base_url = `${realm_url}/api/v1/external/`; const base_url = `${realm_url}/api/v1/external/`;
$integration_url.text(`${base_url}${selected_integration}?${params.toString()}`); $integration_url.text(`${base_url}${selected_integration}?${params.toString()}`);
@ -181,6 +253,15 @@ export function show_generate_integration_url_modal(api_key: string): void {
integration_input_dropdown_widget.render(); integration_input_dropdown_widget.render();
$(".integration-url-name-wrapper").trigger("input"); $(".integration-url-name-wrapper").trigger("input");
const selected_integration = integration_input_dropdown_widget.value();
const selected_integration_data = realm.realm_incoming_webhook_bots.find(
(bot) => bot.name === selected_integration,
);
if (selected_integration_data?.config_options) {
render_config(selected_integration_data.config_options);
}
dropdown.hide(); dropdown.hide();
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
@ -265,6 +346,7 @@ export function show_generate_integration_url_modal(api_key: string): void {
$topic_input.parent().addClass("hide"); $topic_input.parent().addClass("hide");
stream_input_dropdown_widget.render(direct_messages_option.unique_id); stream_input_dropdown_widget.render(direct_messages_option.unique_id);
$config_container.empty();
} }
} }

View File

@ -342,7 +342,15 @@ export const realm_schema = z.object({
display_name: z.string(), display_name: z.string(),
name: z.string(), name: z.string(),
all_event_types: z.nullable(z.array(z.string())), all_event_types: z.nullable(z.array(z.string())),
// We currently ignore the `config` field in these objects. config_options: z
.array(
z.object({
key: z.string(),
label: z.string(),
validator: z.string(),
}),
)
.optional(),
}), }),
), ),
realm_inline_image_preview: z.boolean(), realm_inline_image_preview: z.boolean(),

View File

@ -0,0 +1,7 @@
<div class="input-group" id="integration-url-{{key}}-container">
<label class="checkbox">
<input type="checkbox" id="integration-url-{{key}}-checkbox" class="integration-url-parameter" />
<span class="rendered-checkbox"></span>
</label>
<label class="inline" for="integration-url-{{key}}-checkbox">{{label}}</label>
</div>

View File

@ -0,0 +1,4 @@
<div class="input-group" id="integration-url-{{key}}-container">
<label for="integration-url-{{key}}-text" class="modal-label-field">{{label}}</label>
<input type="text" id="integration-url-{{key}}-text" class="modal_text_input integration-url-parameter" value=""/>
</div>

View File

@ -25,6 +25,9 @@
<label for="integration-url-topic-input" class="modal-label-field">{{t "Topic"}}</label> <label for="integration-url-topic-input" class="modal-label-field">{{t "Topic"}}</label>
<input type="text" id="integration-url-topic-input" class="modal_text_input integration-url-parameter" maxlength="{{ max_topic_length }}" /> <input type="text" id="integration-url-topic-input" class="modal_text_input integration-url-parameter" maxlength="{{ max_topic_length }}" />
</div> </div>
<div id="integration-url-config-options-container">
<!-- Dynamic Config Options will be rendered here -->
</div>
<div id="integration-events-parameter" class="input-group hide"> <div id="integration-events-parameter" class="input-group hide">
<label class="checkbox"> <label class="checkbox">
<input type="checkbox" id="show-integration-events"/> <input type="checkbox" id="show-integration-events"/>

View File

@ -621,7 +621,16 @@ def fetch_initial_state_data(
"name": integration.name, "name": integration.name,
"display_name": integration.display_name, "display_name": integration.display_name,
"all_event_types": get_all_event_types_for_integration(integration), "all_event_types": get_all_event_types_for_integration(integration),
"config": {c[1]: c[0] for c in integration.config_options}, "config_options": [
{
"key": c.name,
"label": c.description,
"validator": c.validator.__name__,
}
for c in integration.config_options
]
if integration.config_options
else [],
} }
for integration in WEBHOOK_INTEGRATIONS for integration in WEBHOOK_INTEGRATIONS
if integration.legacy is False if integration.legacy is False

View File

@ -12,6 +12,8 @@ from django.views.decorators.csrf import csrf_exempt
from django_stubs_ext import StrPromise from django_stubs_ext import StrPromise
from zerver.lib.storage import static_path from zerver.lib.storage import static_path
from zerver.lib.validator import check_bool, check_string
from zerver.lib.webhooks.common import WebhookConfigOption
"""This module declares all of the (documented) integrations available """This module declares all of the (documented) integrations available
in the Zulip server. The Integration class is used as part of in the Zulip server. The Integration class is used as part of
@ -34,7 +36,7 @@ Over time, we expect this registry to grow additional convenience
features for writing and configuring integrations efficiently. features for writing and configuring integrations efficiently.
""" """
OptionValidator: TypeAlias = Callable[[str, str], str | None] OptionValidator: TypeAlias = Callable[[str, str], str | bool | None]
META_CATEGORY: dict[str, StrPromise] = { META_CATEGORY: dict[str, StrPromise] = {
"meta-integration": gettext_lazy("Integration frameworks"), "meta-integration": gettext_lazy("Integration frameworks"),
@ -75,7 +77,7 @@ class Integration:
doc: str | None = None, doc: str | None = None,
stream_name: str | None = None, stream_name: str | None = None,
legacy: bool = False, legacy: bool = False,
config_options: Sequence[tuple[str, str, OptionValidator]] = [], config_options: Sequence[WebhookConfigOption] = [],
) -> None: ) -> None:
self.name = name self.name = name
self.client_name = client_name self.client_name = client_name
@ -198,7 +200,7 @@ class WebhookIntegration(Integration):
doc: str | None = None, doc: str | None = None,
stream_name: str | None = None, stream_name: str | None = None,
legacy: bool = False, legacy: bool = False,
config_options: Sequence[tuple[str, str, OptionValidator]] = [], config_options: Sequence[WebhookConfigOption] = [],
dir_name: str | None = None, dir_name: str | None = None,
) -> None: ) -> None:
if client_name is None: if client_name is None:
@ -408,6 +410,18 @@ WEBHOOK_INTEGRATIONS: list[WebhookIntegration] = [
logo="images/integrations/logos/github.svg", logo="images/integrations/logos/github.svg",
function="zerver.webhooks.github.view.api_github_webhook", function="zerver.webhooks.github.view.api_github_webhook",
stream_name="github", stream_name="github",
config_options=[
WebhookConfigOption(
name="branches",
description="Filter by branches (comma-separated list)",
validator=check_string,
),
WebhookConfigOption(
name="ignore_private_repositories",
description="Exclude notifications from private repositories",
validator=check_bool,
),
],
), ),
WebhookIntegration( WebhookIntegration(
"githubsponsors", "githubsponsors",

View File

@ -130,7 +130,9 @@ def check_valid_bot_config(
for integration in WEBHOOK_INTEGRATIONS: for integration in WEBHOOK_INTEGRATIONS:
if integration.name == service_name: if integration.name == service_name:
# key: validator # key: validator
config_options = {c[1]: c[2] for c in integration.config_options} config_options = {
option.name: option.validator for option in integration.config_options
}
break break
if not config_options: if not config_options:
raise JsonableError( raise JsonableError(

View File

@ -1,6 +1,7 @@
import fnmatch import fnmatch
import importlib import importlib
from collections.abc import Callable from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime from datetime import datetime
from typing import Annotated, Any, TypeAlias from typing import Annotated, Any, TypeAlias
from urllib.parse import unquote from urllib.parse import unquote
@ -49,6 +50,13 @@ SETUP_MESSAGE_USER_PART = " by {user_name}"
OptionalUserSpecifiedTopicStr: TypeAlias = Annotated[str | None, ApiParamConfig("topic")] OptionalUserSpecifiedTopicStr: TypeAlias = Annotated[str | None, ApiParamConfig("topic")]
@dataclass
class WebhookConfigOption:
name: str
description: str
validator: Callable[[str, str], str | bool | None]
def get_setup_webhook_message(integration: str, user_name: str | None = None) -> str: def get_setup_webhook_message(integration: str, user_name: str | None = None) -> str:
content = SETUP_MESSAGE_TEMPLATE.format(integration=integration) content = SETUP_MESSAGE_TEMPLATE.format(integration=integration)
if user_name: if user_name:

View File

@ -14714,8 +14714,8 @@ paths:
such filtering. such filtering.
**Changes**: New in Zulip 8.0 (feature level 207). **Changes**: New in Zulip 8.0 (feature level 207).
config: config_options:
$ref: "#/components/schemas/BotConfiguration" $ref: "#/components/schemas/WebhookConfigOption"
recent_private_conversations: recent_private_conversations:
description: | description: |
Present if `recent_private_conversations` is present in `fetch_event_types`. Present if `recent_private_conversations` is present in `fetch_event_types`.
@ -21990,6 +21990,36 @@ components:
description: | description: |
`{config_key}`: Description/value of the configuration data key. `{config_key}`: Description/value of the configuration data key.
type: string type: string
WebhookConfigOption:
type: array
description: |
An array of configuration options where each option is an
object containing a unique identifier, a human-readable name,
and a validation function name hinting how to verify the
correct input format.
This is an unstable API expected to be used only by the Zulip web
apps. Please discuss in chat.zulip.org before using it.
**Changes**: New in Zulip 10.0 (feature level 318).
items:
type: object
additionalProperties: false
properties:
key:
type: string
description: |
A key for the configuration option to use in generated URLs.
label:
type: string
description: |
A human-readable label of the configuration option.
validator:
type: string
description: |
The name of the validator function for the configuration
option. Currently generated values are `check_bool` and
`check_str`.
CustomProfileField: CustomProfileField:
type: object type: object
additionalProperties: false additionalProperties: false

View File

@ -18,6 +18,7 @@ from zerver.lib.integrations import EMBEDDED_BOTS, WebhookIntegration
from zerver.lib.test_classes import UploadSerializeMixin, ZulipTestCase from zerver.lib.test_classes import UploadSerializeMixin, ZulipTestCase
from zerver.lib.test_helpers import avatar_disk_path, get_test_image_file from zerver.lib.test_helpers import avatar_disk_path, get_test_image_file
from zerver.lib.utils import assert_is_not_none from zerver.lib.utils import assert_is_not_none
from zerver.lib.webhooks.common import WebhookConfigOption
from zerver.models import RealmUserDefault, Service, Subscription, UserProfile from zerver.models import RealmUserDefault, Service, Subscription, UserProfile
from zerver.models.bots import get_bot_services from zerver.models.bots import get_bot_services
from zerver.models.realms import BotCreationPolicyEnum, get_realm from zerver.models.realms import BotCreationPolicyEnum, get_realm
@ -37,7 +38,11 @@ stripe_sample_config_options = [
"stripe", "stripe",
["financial"], ["financial"],
display_name="Stripe", display_name="Stripe",
config_options=[("Stripe API key", "stripe_api_key", _check_string)], config_options=[
WebhookConfigOption(
name="stripe_api_key", description="Stripe API key", validator=_check_string
)
],
), ),
] ]

View File

@ -0,0 +1,412 @@
{
"action": "closed",
"number": 1,
"pull_request": {
"url": "https://api.github.com/repos/baxterthehacker/public-repo/pulls/1",
"id": 34778301,
"html_url": "https://github.com/baxterthehacker/public-repo/pull/1",
"diff_url": "https://github.com/baxterthehacker/public-repo/pull/1.diff",
"patch_url": "https://github.com/baxterthehacker/public-repo/pull/1.patch",
"issue_url": "https://api.github.com/repos/baxterthehacker/public-repo/issues/1",
"number": 1,
"state": "merged",
"locked": false,
"title": "Update the README with new information",
"user": {
"login": "baxterthehacker",
"id": 6752317,
"avatar_url": "https://avatars.githubusercontent.com/u/6752317?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/baxterthehacker",
"html_url": "https://github.com/baxterthehacker",
"followers_url": "https://api.github.com/users/baxterthehacker/followers",
"following_url": "https://api.github.com/users/baxterthehacker/following{/other_user}",
"gists_url": "https://api.github.com/users/baxterthehacker/gists{/gist_id}",
"starred_url": "https://api.github.com/users/baxterthehacker/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/baxterthehacker/subscriptions",
"organizations_url": "https://api.github.com/users/baxterthehacker/orgs",
"repos_url": "https://api.github.com/users/baxterthehacker/repos",
"events_url": "https://api.github.com/users/baxterthehacker/events{/privacy}",
"received_events_url": "https://api.github.com/users/baxterthehacker/received_events",
"type": "User",
"site_admin": false
},
"body": "This is a pretty simple change that we need to pull into master.",
"created_at": "2015-05-05T23:40:27Z",
"updated_at": "2015-05-05T23:40:27Z",
"closed_at": null,
"merged_at": null,
"merge_commit_sha": null,
"assignee": null,
"milestone": null,
"commits_url": "https://api.github.com/repos/baxterthehacker/public-repo/pulls/1/commits",
"review_comments_url": "https://api.github.com/repos/baxterthehacker/public-repo/pulls/1/comments",
"review_comment_url": "https://api.github.com/repos/baxterthehacker/public-repo/pulls/comments{/number}",
"comments_url": "https://api.github.com/repos/baxterthehacker/public-repo/issues/1/comments",
"statuses_url": "https://api.github.com/repos/baxterthehacker/public-repo/statuses/0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c",
"head": {
"label": "baxterthehacker:changes",
"ref": "changes",
"sha": "0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c",
"user": {
"login": "baxterthehacker",
"id": 6752317,
"avatar_url": "https://avatars.githubusercontent.com/u/6752317?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/baxterthehacker",
"html_url": "https://github.com/baxterthehacker",
"followers_url": "https://api.github.com/users/baxterthehacker/followers",
"following_url": "https://api.github.com/users/baxterthehacker/following{/other_user}",
"gists_url": "https://api.github.com/users/baxterthehacker/gists{/gist_id}",
"starred_url": "https://api.github.com/users/baxterthehacker/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/baxterthehacker/subscriptions",
"organizations_url": "https://api.github.com/users/baxterthehacker/orgs",
"repos_url": "https://api.github.com/users/baxterthehacker/repos",
"events_url": "https://api.github.com/users/baxterthehacker/events{/privacy}",
"received_events_url": "https://api.github.com/users/baxterthehacker/received_events",
"type": "User",
"site_admin": false
},
"repo": {
"id": 35129377,
"name": "public-repo",
"full_name": "baxterthehacker/public-repo",
"owner": {
"login": "baxterthehacker",
"id": 6752317,
"avatar_url": "https://avatars.githubusercontent.com/u/6752317?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/baxterthehacker",
"html_url": "https://github.com/baxterthehacker",
"followers_url": "https://api.github.com/users/baxterthehacker/followers",
"following_url": "https://api.github.com/users/baxterthehacker/following{/other_user}",
"gists_url": "https://api.github.com/users/baxterthehacker/gists{/gist_id}",
"starred_url": "https://api.github.com/users/baxterthehacker/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/baxterthehacker/subscriptions",
"organizations_url": "https://api.github.com/users/baxterthehacker/orgs",
"repos_url": "https://api.github.com/users/baxterthehacker/repos",
"events_url": "https://api.github.com/users/baxterthehacker/events{/privacy}",
"received_events_url": "https://api.github.com/users/baxterthehacker/received_events",
"type": "User",
"site_admin": false
},
"private": true,
"html_url": "https://github.com/baxterthehacker/public-repo",
"description": "",
"fork": false,
"url": "https://api.github.com/repos/baxterthehacker/public-repo",
"forks_url": "https://api.github.com/repos/baxterthehacker/public-repo/forks",
"keys_url": "https://api.github.com/repos/baxterthehacker/public-repo/keys{/key_id}",
"collaborators_url": "https://api.github.com/repos/baxterthehacker/public-repo/collaborators{/collaborator}",
"teams_url": "https://api.github.com/repos/baxterthehacker/public-repo/teams",
"hooks_url": "https://api.github.com/repos/baxterthehacker/public-repo/hooks",
"issue_events_url": "https://api.github.com/repos/baxterthehacker/public-repo/issues/events{/number}",
"events_url": "https://api.github.com/repos/baxterthehacker/public-repo/events",
"assignees_url": "https://api.github.com/repos/baxterthehacker/public-repo/assignees{/user}",
"branches_url": "https://api.github.com/repos/baxterthehacker/public-repo/branches{/branch}",
"tags_url": "https://api.github.com/repos/baxterthehacker/public-repo/tags",
"blobs_url": "https://api.github.com/repos/baxterthehacker/public-repo/git/blobs{/sha}",
"git_tags_url": "https://api.github.com/repos/baxterthehacker/public-repo/git/tags{/sha}",
"git_refs_url": "https://api.github.com/repos/baxterthehacker/public-repo/git/refs{/sha}",
"trees_url": "https://api.github.com/repos/baxterthehacker/public-repo/git/trees{/sha}",
"statuses_url": "https://api.github.com/repos/baxterthehacker/public-repo/statuses/{sha}",
"languages_url": "https://api.github.com/repos/baxterthehacker/public-repo/languages",
"stargazers_url": "https://api.github.com/repos/baxterthehacker/public-repo/stargazers",
"contributors_url": "https://api.github.com/repos/baxterthehacker/public-repo/contributors",
"subscribers_url": "https://api.github.com/repos/baxterthehacker/public-repo/subscribers",
"subscription_url": "https://api.github.com/repos/baxterthehacker/public-repo/subscription",
"commits_url": "https://api.github.com/repos/baxterthehacker/public-repo/commits{/sha}",
"git_commits_url": "https://api.github.com/repos/baxterthehacker/public-repo/git/commits{/sha}",
"comments_url": "https://api.github.com/repos/baxterthehacker/public-repo/comments{/number}",
"issue_comment_url": "https://api.github.com/repos/baxterthehacker/public-repo/issues/comments{/number}",
"contents_url": "https://api.github.com/repos/baxterthehacker/public-repo/contents/{+path}",
"compare_url": "https://api.github.com/repos/baxterthehacker/public-repo/compare/{base}...{head}",
"merges_url": "https://api.github.com/repos/baxterthehacker/public-repo/merges",
"archive_url": "https://api.github.com/repos/baxterthehacker/public-repo/{archive_format}{/ref}",
"downloads_url": "https://api.github.com/repos/baxterthehacker/public-repo/downloads",
"issues_url": "https://api.github.com/repos/baxterthehacker/public-repo/issues{/number}",
"pulls_url": "https://api.github.com/repos/baxterthehacker/public-repo/pulls{/number}",
"milestones_url": "https://api.github.com/repos/baxterthehacker/public-repo/milestones{/number}",
"notifications_url": "https://api.github.com/repos/baxterthehacker/public-repo/notifications{?since,all,participating}",
"labels_url": "https://api.github.com/repos/baxterthehacker/public-repo/labels{/name}",
"releases_url": "https://api.github.com/repos/baxterthehacker/public-repo/releases{/id}",
"created_at": "2015-05-05T23:40:12Z",
"updated_at": "2015-05-05T23:40:12Z",
"pushed_at": "2015-05-05T23:40:26Z",
"git_url": "git://github.com/baxterthehacker/public-repo.git",
"ssh_url": "git@github.com:baxterthehacker/public-repo.git",
"clone_url": "https://github.com/baxterthehacker/public-repo.git",
"svn_url": "https://github.com/baxterthehacker/public-repo",
"homepage": null,
"size": 0,
"stargazers_count": 0,
"watchers_count": 0,
"language": null,
"has_issues": true,
"has_downloads": true,
"has_wiki": true,
"has_pages": true,
"forks_count": 0,
"mirror_url": null,
"open_issues_count": 1,
"forks": 0,
"open_issues": 1,
"watchers": 0,
"default_branch": "master"
}
},
"base": {
"label": "baxterthehacker:master",
"ref": "master",
"sha": "9049f1265b7d61be4a8904a9a27120d2064dab3b",
"user": {
"login": "baxterthehacker",
"id": 6752317,
"avatar_url": "https://avatars.githubusercontent.com/u/6752317?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/baxterthehacker",
"html_url": "https://github.com/baxterthehacker",
"followers_url": "https://api.github.com/users/baxterthehacker/followers",
"following_url": "https://api.github.com/users/baxterthehacker/following{/other_user}",
"gists_url": "https://api.github.com/users/baxterthehacker/gists{/gist_id}",
"starred_url": "https://api.github.com/users/baxterthehacker/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/baxterthehacker/subscriptions",
"organizations_url": "https://api.github.com/users/baxterthehacker/orgs",
"repos_url": "https://api.github.com/users/baxterthehacker/repos",
"events_url": "https://api.github.com/users/baxterthehacker/events{/privacy}",
"received_events_url": "https://api.github.com/users/baxterthehacker/received_events",
"type": "User",
"site_admin": false
},
"repo": {
"id": 35129377,
"name": "public-repo",
"full_name": "baxterthehacker/public-repo",
"owner": {
"login": "baxterthehacker",
"id": 6752317,
"avatar_url": "https://avatars.githubusercontent.com/u/6752317?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/baxterthehacker",
"html_url": "https://github.com/baxterthehacker",
"followers_url": "https://api.github.com/users/baxterthehacker/followers",
"following_url": "https://api.github.com/users/baxterthehacker/following{/other_user}",
"gists_url": "https://api.github.com/users/baxterthehacker/gists{/gist_id}",
"starred_url": "https://api.github.com/users/baxterthehacker/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/baxterthehacker/subscriptions",
"organizations_url": "https://api.github.com/users/baxterthehacker/orgs",
"repos_url": "https://api.github.com/users/baxterthehacker/repos",
"events_url": "https://api.github.com/users/baxterthehacker/events{/privacy}",
"received_events_url": "https://api.github.com/users/baxterthehacker/received_events",
"type": "User",
"site_admin": false
},
"private": false,
"html_url": "https://github.com/baxterthehacker/public-repo",
"description": "",
"fork": false,
"url": "https://api.github.com/repos/baxterthehacker/public-repo",
"forks_url": "https://api.github.com/repos/baxterthehacker/public-repo/forks",
"keys_url": "https://api.github.com/repos/baxterthehacker/public-repo/keys{/key_id}",
"collaborators_url": "https://api.github.com/repos/baxterthehacker/public-repo/collaborators{/collaborator}",
"teams_url": "https://api.github.com/repos/baxterthehacker/public-repo/teams",
"hooks_url": "https://api.github.com/repos/baxterthehacker/public-repo/hooks",
"issue_events_url": "https://api.github.com/repos/baxterthehacker/public-repo/issues/events{/number}",
"events_url": "https://api.github.com/repos/baxterthehacker/public-repo/events",
"assignees_url": "https://api.github.com/repos/baxterthehacker/public-repo/assignees{/user}",
"branches_url": "https://api.github.com/repos/baxterthehacker/public-repo/branches{/branch}",
"tags_url": "https://api.github.com/repos/baxterthehacker/public-repo/tags",
"blobs_url": "https://api.github.com/repos/baxterthehacker/public-repo/git/blobs{/sha}",
"git_tags_url": "https://api.github.com/repos/baxterthehacker/public-repo/git/tags{/sha}",
"git_refs_url": "https://api.github.com/repos/baxterthehacker/public-repo/git/refs{/sha}",
"trees_url": "https://api.github.com/repos/baxterthehacker/public-repo/git/trees{/sha}",
"statuses_url": "https://api.github.com/repos/baxterthehacker/public-repo/statuses/{sha}",
"languages_url": "https://api.github.com/repos/baxterthehacker/public-repo/languages",
"stargazers_url": "https://api.github.com/repos/baxterthehacker/public-repo/stargazers",
"contributors_url": "https://api.github.com/repos/baxterthehacker/public-repo/contributors",
"subscribers_url": "https://api.github.com/repos/baxterthehacker/public-repo/subscribers",
"subscription_url": "https://api.github.com/repos/baxterthehacker/public-repo/subscription",
"commits_url": "https://api.github.com/repos/baxterthehacker/public-repo/commits{/sha}",
"git_commits_url": "https://api.github.com/repos/baxterthehacker/public-repo/git/commits{/sha}",
"comments_url": "https://api.github.com/repos/baxterthehacker/public-repo/comments{/number}",
"issue_comment_url": "https://api.github.com/repos/baxterthehacker/public-repo/issues/comments{/number}",
"contents_url": "https://api.github.com/repos/baxterthehacker/public-repo/contents/{+path}",
"compare_url": "https://api.github.com/repos/baxterthehacker/public-repo/compare/{base}...{head}",
"merges_url": "https://api.github.com/repos/baxterthehacker/public-repo/merges",
"archive_url": "https://api.github.com/repos/baxterthehacker/public-repo/{archive_format}{/ref}",
"downloads_url": "https://api.github.com/repos/baxterthehacker/public-repo/downloads",
"issues_url": "https://api.github.com/repos/baxterthehacker/public-repo/issues{/number}",
"pulls_url": "https://api.github.com/repos/baxterthehacker/public-repo/pulls{/number}",
"milestones_url": "https://api.github.com/repos/baxterthehacker/public-repo/milestones{/number}",
"notifications_url": "https://api.github.com/repos/baxterthehacker/public-repo/notifications{?since,all,participating}",
"labels_url": "https://api.github.com/repos/baxterthehacker/public-repo/labels{/name}",
"releases_url": "https://api.github.com/repos/baxterthehacker/public-repo/releases{/id}",
"created_at": "2015-05-05T23:40:12Z",
"updated_at": "2015-05-05T23:40:12Z",
"pushed_at": "2015-05-05T23:40:26Z",
"git_url": "git://github.com/baxterthehacker/public-repo.git",
"ssh_url": "git@github.com:baxterthehacker/public-repo.git",
"clone_url": "https://github.com/baxterthehacker/public-repo.git",
"svn_url": "https://github.com/baxterthehacker/public-repo",
"homepage": null,
"size": 0,
"stargazers_count": 0,
"watchers_count": 0,
"language": null,
"has_issues": true,
"has_downloads": true,
"has_wiki": true,
"has_pages": true,
"forks_count": 0,
"mirror_url": null,
"open_issues_count": 1,
"forks": 0,
"open_issues": 1,
"watchers": 0,
"default_branch": "master"
}
},
"_links": {
"self": {
"href": "https://api.github.com/repos/baxterthehacker/public-repo/pulls/1"
},
"html": {
"href": "https://github.com/baxterthehacker/public-repo/pull/1"
},
"issue": {
"href": "https://api.github.com/repos/baxterthehacker/public-repo/issues/1"
},
"comments": {
"href": "https://api.github.com/repos/baxterthehacker/public-repo/issues/1/comments"
},
"review_comments": {
"href": "https://api.github.com/repos/baxterthehacker/public-repo/pulls/1/comments"
},
"review_comment": {
"href": "https://api.github.com/repos/baxterthehacker/public-repo/pulls/comments{/number}"
},
"commits": {
"href": "https://api.github.com/repos/baxterthehacker/public-repo/pulls/1/commits"
},
"statuses": {
"href": "https://api.github.com/repos/baxterthehacker/public-repo/statuses/0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c"
}
},
"merged": true,
"mergeable": null,
"mergeable_state": "unknown",
"merged_by": null,
"comments": 0,
"review_comments": 0,
"commits": 1,
"additions": 1,
"deletions": 1,
"changed_files": 1
},
"repository": {
"id": 35129377,
"name": "public-repo",
"full_name": "baxterthehacker/public-repo",
"owner": {
"login": "baxterthehacker",
"id": 6752317,
"avatar_url": "https://avatars.githubusercontent.com/u/6752317?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/baxterthehacker",
"html_url": "https://github.com/baxterthehacker",
"followers_url": "https://api.github.com/users/baxterthehacker/followers",
"following_url": "https://api.github.com/users/baxterthehacker/following{/other_user}",
"gists_url": "https://api.github.com/users/baxterthehacker/gists{/gist_id}",
"starred_url": "https://api.github.com/users/baxterthehacker/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/baxterthehacker/subscriptions",
"organizations_url": "https://api.github.com/users/baxterthehacker/orgs",
"repos_url": "https://api.github.com/users/baxterthehacker/repos",
"events_url": "https://api.github.com/users/baxterthehacker/events{/privacy}",
"received_events_url": "https://api.github.com/users/baxterthehacker/received_events",
"type": "User",
"site_admin": false
},
"private": true,
"html_url": "https://github.com/baxterthehacker/public-repo",
"description": "",
"fork": false,
"url": "https://api.github.com/repos/baxterthehacker/public-repo",
"forks_url": "https://api.github.com/repos/baxterthehacker/public-repo/forks",
"keys_url": "https://api.github.com/repos/baxterthehacker/public-repo/keys{/key_id}",
"collaborators_url": "https://api.github.com/repos/baxterthehacker/public-repo/collaborators{/collaborator}",
"teams_url": "https://api.github.com/repos/baxterthehacker/public-repo/teams",
"hooks_url": "https://api.github.com/repos/baxterthehacker/public-repo/hooks",
"issue_events_url": "https://api.github.com/repos/baxterthehacker/public-repo/issues/events{/number}",
"events_url": "https://api.github.com/repos/baxterthehacker/public-repo/events",
"assignees_url": "https://api.github.com/repos/baxterthehacker/public-repo/assignees{/user}",
"branches_url": "https://api.github.com/repos/baxterthehacker/public-repo/branches{/branch}",
"tags_url": "https://api.github.com/repos/baxterthehacker/public-repo/tags",
"blobs_url": "https://api.github.com/repos/baxterthehacker/public-repo/git/blobs{/sha}",
"git_tags_url": "https://api.github.com/repos/baxterthehacker/public-repo/git/tags{/sha}",
"git_refs_url": "https://api.github.com/repos/baxterthehacker/public-repo/git/refs{/sha}",
"trees_url": "https://api.github.com/repos/baxterthehacker/public-repo/git/trees{/sha}",
"statuses_url": "https://api.github.com/repos/baxterthehacker/public-repo/statuses/{sha}",
"languages_url": "https://api.github.com/repos/baxterthehacker/public-repo/languages",
"stargazers_url": "https://api.github.com/repos/baxterthehacker/public-repo/stargazers",
"contributors_url": "https://api.github.com/repos/baxterthehacker/public-repo/contributors",
"subscribers_url": "https://api.github.com/repos/baxterthehacker/public-repo/subscribers",
"subscription_url": "https://api.github.com/repos/baxterthehacker/public-repo/subscription",
"commits_url": "https://api.github.com/repos/baxterthehacker/public-repo/commits{/sha}",
"git_commits_url": "https://api.github.com/repos/baxterthehacker/public-repo/git/commits{/sha}",
"comments_url": "https://api.github.com/repos/baxterthehacker/public-repo/comments{/number}",
"issue_comment_url": "https://api.github.com/repos/baxterthehacker/public-repo/issues/comments{/number}",
"contents_url": "https://api.github.com/repos/baxterthehacker/public-repo/contents/{+path}",
"compare_url": "https://api.github.com/repos/baxterthehacker/public-repo/compare/{base}...{head}",
"merges_url": "https://api.github.com/repos/baxterthehacker/public-repo/merges",
"archive_url": "https://api.github.com/repos/baxterthehacker/public-repo/{archive_format}{/ref}",
"downloads_url": "https://api.github.com/repos/baxterthehacker/public-repo/downloads",
"issues_url": "https://api.github.com/repos/baxterthehacker/public-repo/issues{/number}",
"pulls_url": "https://api.github.com/repos/baxterthehacker/public-repo/pulls{/number}",
"milestones_url": "https://api.github.com/repos/baxterthehacker/public-repo/milestones{/number}",
"notifications_url": "https://api.github.com/repos/baxterthehacker/public-repo/notifications{?since,all,participating}",
"labels_url": "https://api.github.com/repos/baxterthehacker/public-repo/labels{/name}",
"releases_url": "https://api.github.com/repos/baxterthehacker/public-repo/releases{/id}",
"created_at": "2015-05-05T23:40:12Z",
"updated_at": "2015-05-05T23:40:12Z",
"pushed_at": "2015-05-05T23:40:26Z",
"git_url": "git://github.com/baxterthehacker/public-repo.git",
"ssh_url": "git@github.com:baxterthehacker/public-repo.git",
"clone_url": "https://github.com/baxterthehacker/public-repo.git",
"svn_url": "https://github.com/baxterthehacker/public-repo",
"homepage": null,
"size": 0,
"stargazers_count": 0,
"watchers_count": 0,
"language": null,
"has_issues": true,
"has_downloads": true,
"has_wiki": true,
"has_pages": true,
"forks_count": 0,
"mirror_url": null,
"open_issues_count": 1,
"forks": 0,
"open_issues": 1,
"watchers": 0,
"default_branch": "master"
},
"sender": {
"login": "baxterthehacker",
"id": 6752317,
"avatar_url": "https://avatars.githubusercontent.com/u/6752317?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/baxterthehacker",
"html_url": "https://github.com/baxterthehacker",
"followers_url": "https://api.github.com/users/baxterthehacker/followers",
"following_url": "https://api.github.com/users/baxterthehacker/following{/other_user}",
"gists_url": "https://api.github.com/users/baxterthehacker/gists{/gist_id}",
"starred_url": "https://api.github.com/users/baxterthehacker/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/baxterthehacker/subscriptions",
"organizations_url": "https://api.github.com/users/baxterthehacker/orgs",
"repos_url": "https://api.github.com/users/baxterthehacker/repos",
"events_url": "https://api.github.com/users/baxterthehacker/events{/privacy}",
"received_events_url": "https://api.github.com/users/baxterthehacker/received_events",
"type": "User",
"site_admin": false
}
}

View File

@ -0,0 +1,163 @@
{
"ref": "refs/heads/changes",
"before": "9049f1265b7d61be4a8904a9a27120d2064dab3b",
"after": "0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c",
"created": false,
"deleted": false,
"forced": false,
"base_ref": null,
"compare": "https://github.com/baxterthehacker/private-repo/compare/9049f1265b7d...0d1a26e67d8f",
"commits": [
{
"id": "0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c",
"tree_id": "f9d2a07e9488b91af2641b26b9407fe22a451433",
"distinct": true,
"message": "Update README.md",
"timestamp": "2015-05-05T19:40:15-04:00",
"url": "https://github.com/baxterthehacker/private-repo/commit/0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c",
"author": {
"name": "baxterthehacker",
"email": "baxterthehacker@users.noreply.github.com",
"username": "baxterthehacker"
},
"committer": {
"name": "baxterthehacker",
"email": "baxterthehacker@users.noreply.github.com",
"username": "baxterthehacker"
},
"added": [
],
"removed": [
],
"modified": [
"README.md"
]
}
],
"head_commit": {
"id": "0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c",
"tree_id": "f9d2a07e9488b91af2641b26b9407fe22a451433",
"distinct": true,
"message": "Update README.md",
"timestamp": "2015-05-05T19:40:15-04:00",
"url": "https://github.com/baxterthehacker/private-repo/commit/0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c",
"author": {
"name": "baxterthehacker",
"email": "baxterthehacker@users.noreply.github.com",
"username": "baxterthehacker"
},
"committer": {
"name": "baxterthehacker",
"email": "baxterthehacker@users.noreply.github.com",
"username": "baxterthehacker"
},
"added": [
],
"removed": [
],
"modified": [
"README.md"
]
},
"repository": {
"id": 35129377,
"name": "private-repo",
"full_name": "baxterthehacker/private-repo",
"owner": {
"name": "baxterthehacker",
"email": "baxterthehacker@users.noreply.github.com"
},
"private": true,
"html_url": "https://github.com/baxterthehacker/private-repo",
"description": "",
"fork": false,
"url": "https://github.com/baxterthehacker/private-repo",
"forks_url": "https://api.github.com/repos/baxterthehacker/private-repo/forks",
"keys_url": "https://api.github.com/repos/baxterthehacker/private-repo/keys{/key_id}",
"collaborators_url": "https://api.github.com/repos/baxterthehacker/private-repo/collaborators{/collaborator}",
"teams_url": "https://api.github.com/repos/baxterthehacker/private-repo/teams",
"hooks_url": "https://api.github.com/repos/baxterthehacker/private-repo/hooks",
"issue_events_url": "https://api.github.com/repos/baxterthehacker/private-repo/issues/events{/number}",
"events_url": "https://api.github.com/repos/baxterthehacker/private-repo/events",
"assignees_url": "https://api.github.com/repos/baxterthehacker/private-repo/assignees{/user}",
"branches_url": "https://api.github.com/repos/baxterthehacker/private-repo/branches{/branch}",
"tags_url": "https://api.github.com/repos/baxterthehacker/private-repo/tags",
"blobs_url": "https://api.github.com/repos/baxterthehacker/private-repo/git/blobs{/sha}",
"git_tags_url": "https://api.github.com/repos/baxterthehacker/private-repo/git/tags{/sha}",
"git_refs_url": "https://api.github.com/repos/baxterthehacker/private-repo/git/refs{/sha}",
"trees_url": "https://api.github.com/repos/baxterthehacker/private-repo/git/trees{/sha}",
"statuses_url": "https://api.github.com/repos/baxterthehacker/private-repo/statuses/{sha}",
"languages_url": "https://api.github.com/repos/baxterthehacker/private-repo/languages",
"stargazers_url": "https://api.github.com/repos/baxterthehacker/private-repo/stargazers",
"contributors_url": "https://api.github.com/repos/baxterthehacker/private-repo/contributors",
"subscribers_url": "https://api.github.com/repos/baxterthehacker/private-repo/subscribers",
"subscription_url": "https://api.github.com/repos/baxterthehacker/private-repo/subscription",
"commits_url": "https://api.github.com/repos/baxterthehacker/private-repo/commits{/sha}",
"git_commits_url": "https://api.github.com/repos/baxterthehacker/private-repo/git/commits{/sha}",
"comments_url": "https://api.github.com/repos/baxterthehacker/private-repo/comments{/number}",
"issue_comment_url": "https://api.github.com/repos/baxterthehacker/private-repo/issues/comments{/number}",
"contents_url": "https://api.github.com/repos/baxterthehacker/private-repo/contents/{+path}",
"compare_url": "https://api.github.com/repos/baxterthehacker/private-repo/compare/{base}...{head}",
"merges_url": "https://api.github.com/repos/baxterthehacker/private-repo/merges",
"archive_url": "https://api.github.com/repos/baxterthehacker/private-repo/{archive_format}{/ref}",
"downloads_url": "https://api.github.com/repos/baxterthehacker/private-repo/downloads",
"issues_url": "https://api.github.com/repos/baxterthehacker/private-repo/issues{/number}",
"pulls_url": "https://api.github.com/repos/baxterthehacker/private-repo/pulls{/number}",
"milestones_url": "https://api.github.com/repos/baxterthehacker/private-repo/milestones{/number}",
"notifications_url": "https://api.github.com/repos/baxterthehacker/private-repo/notifications{?since,all,participating}",
"labels_url": "https://api.github.com/repos/baxterthehacker/private-repo/labels{/name}",
"releases_url": "https://api.github.com/repos/baxterthehacker/private-repo/releases{/id}",
"created_at": 1430869212,
"updated_at": "2015-05-05T23:40:12Z",
"pushed_at": 1430869217,
"git_url": "git://github.com/baxterthehacker/private-repo.git",
"ssh_url": "git@github.com:baxterthehacker/private-repo.git",
"clone_url": "https://github.com/baxterthehacker/private-repo.git",
"svn_url": "https://github.com/baxterthehacker/private-repo",
"homepage": null,
"size": 0,
"stargazers_count": 0,
"watchers_count": 0,
"language": null,
"has_issues": true,
"has_downloads": true,
"has_wiki": true,
"has_pages": true,
"forks_count": 0,
"mirror_url": null,
"open_issues_count": 0,
"forks": 0,
"open_issues": 0,
"watchers": 0,
"default_branch": "master",
"stargazers": 0,
"master_branch": "master"
},
"pusher": {
"name": "baxterthehacker",
"email": "baxterthehacker@users.noreply.github.com"
},
"sender": {
"login": "baxterthehacker",
"id": 6752317,
"avatar_url": "https://avatars.githubusercontent.com/u/6752317?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/baxterthehacker",
"html_url": "https://github.com/baxterthehacker",
"followers_url": "https://api.github.com/users/baxterthehacker/followers",
"following_url": "https://api.github.com/users/baxterthehacker/following{/other_user}",
"gists_url": "https://api.github.com/users/baxterthehacker/gists{/gist_id}",
"starred_url": "https://api.github.com/users/baxterthehacker/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/baxterthehacker/subscriptions",
"organizations_url": "https://api.github.com/users/baxterthehacker/orgs",
"repos_url": "https://api.github.com/users/baxterthehacker/repos",
"events_url": "https://api.github.com/users/baxterthehacker/events{/privacy}",
"received_events_url": "https://api.github.com/users/baxterthehacker/received_events",
"type": "User",
"site_admin": false
}
}

View File

@ -7,7 +7,7 @@
"node_id": "MDEwOJllG9zcXaRcvknzNYyTzM4OUQD=", "node_id": "MDEwOJllG9zcXaRcvknzNYyTzM4OUQD=",
"name": "infra-core", "name": "infra-core",
"full_name": "some-organization/infra-core", "full_name": "some-organization/infra-core",
"private": true, "private": false,
"owner": { "owner": {
"name": "some-organization", "name": "some-organization",
"email": null, "email": null,

View File

@ -23,7 +23,7 @@
"type": "Organization", "type": "Organization",
"site_admin": false "site_admin": false
}, },
"private": true, "private": false,
"html_url": "https://github.com/baxterandthehackers/public-repo", "html_url": "https://github.com/baxterandthehackers/public-repo",
"description": "", "description": "",
"fork": false, "fork": false,

View File

@ -0,0 +1,119 @@
{
"action": "created",
"repository": {
"id": 27496774,
"name": "public-repo",
"full_name": "baxterandthehackers/public-repo",
"owner": {
"login": "baxterandthehackers",
"id": 7649605,
"avatar_url": "https://avatars.githubusercontent.com/u/7649605?v=3",
"gravatar_id": "",
"url": "https://api.github.com/users/baxterandthehackers",
"html_url": "https://github.com/baxterandthehackers",
"followers_url": "https://api.github.com/users/baxterandthehackers/followers",
"following_url": "https://api.github.com/users/baxterandthehackers/following{/other_user}",
"gists_url": "https://api.github.com/users/baxterandthehackers/gists{/gist_id}",
"starred_url": "https://api.github.com/users/baxterandthehackers/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/baxterandthehackers/subscriptions",
"organizations_url": "https://api.github.com/users/baxterandthehackers/orgs",
"repos_url": "https://api.github.com/users/baxterandthehackers/repos",
"events_url": "https://api.github.com/users/baxterandthehackers/events{/privacy}",
"received_events_url": "https://api.github.com/users/baxterandthehackers/received_events",
"type": "Organization",
"site_admin": false
},
"private": true,
"html_url": "https://github.com/baxterandthehackers/public-repo",
"description": "",
"fork": false,
"url": "https://api.github.com/repos/baxterandthehackers/public-repo",
"forks_url": "https://api.github.com/repos/baxterandthehackers/public-repo/forks",
"keys_url": "https://api.github.com/repos/baxterandthehackers/public-repo/keys{/key_id}",
"collaborators_url": "https://api.github.com/repos/baxterandthehackers/public-repo/collaborators{/collaborator}",
"teams_url": "https://api.github.com/repos/baxterandthehackers/public-repo/teams",
"hooks_url": "https://api.github.com/repos/baxterandthehackers/public-repo/hooks",
"issue_events_url": "https://api.github.com/repos/baxterandthehackers/public-repo/issues/events{/number}",
"events_url": "https://api.github.com/repos/baxterandthehackers/public-repo/events",
"assignees_url": "https://api.github.com/repos/baxterandthehackers/public-repo/assignees{/user}",
"branches_url": "https://api.github.com/repos/baxterandthehackers/public-repo/branches{/branch}",
"tags_url": "https://api.github.com/repos/baxterandthehackers/public-repo/tags",
"blobs_url": "https://api.github.com/repos/baxterandthehackers/public-repo/git/blobs{/sha}",
"git_tags_url": "https://api.github.com/repos/baxterandthehackers/public-repo/git/tags{/sha}",
"git_refs_url": "https://api.github.com/repos/baxterandthehackers/public-repo/git/refs{/sha}",
"trees_url": "https://api.github.com/repos/baxterandthehackers/public-repo/git/trees{/sha}",
"statuses_url": "https://api.github.com/repos/baxterandthehackers/public-repo/statuses/{sha}",
"languages_url": "https://api.github.com/repos/baxterandthehackers/public-repo/languages",
"stargazers_url": "https://api.github.com/repos/baxterandthehackers/public-repo/stargazers",
"contributors_url": "https://api.github.com/repos/baxterandthehackers/public-repo/contributors",
"subscribers_url": "https://api.github.com/repos/baxterandthehackers/public-repo/subscribers",
"subscription_url": "https://api.github.com/repos/baxterandthehackers/public-repo/subscription",
"commits_url": "https://api.github.com/repos/baxterandthehackers/public-repo/commits{/sha}",
"git_commits_url": "https://api.github.com/repos/baxterandthehackers/public-repo/git/commits{/sha}",
"comments_url": "https://api.github.com/repos/baxterandthehackers/public-repo/comments{/number}",
"issue_comment_url": "https://api.github.com/repos/baxterandthehackers/public-repo/issues/comments/{number}",
"contents_url": "https://api.github.com/repos/baxterandthehackers/public-repo/contents/{+path}",
"compare_url": "https://api.github.com/repos/baxterandthehackers/public-repo/compare/{base}...{head}",
"merges_url": "https://api.github.com/repos/baxterandthehackers/public-repo/merges",
"archive_url": "https://api.github.com/repos/baxterandthehackers/public-repo/{archive_format}{/ref}",
"downloads_url": "https://api.github.com/repos/baxterandthehackers/public-repo/downloads",
"issues_url": "https://api.github.com/repos/baxterandthehackers/public-repo/issues{/number}",
"pulls_url": "https://api.github.com/repos/baxterandthehackers/public-repo/pulls{/number}",
"milestones_url": "https://api.github.com/repos/baxterandthehackers/public-repo/milestones{/number}",
"notifications_url": "https://api.github.com/repos/baxterandthehackers/public-repo/notifications{?since,all,participating}",
"labels_url": "https://api.github.com/repos/baxterandthehackers/public-repo/labels{/name}",
"releases_url": "https://api.github.com/repos/baxterandthehackers/public-repo/releases{/id}",
"created_at": "2014-12-03T16:39:25Z",
"updated_at": "2014-12-03T16:39:25Z",
"pushed_at": "2014-12-03T16:39:25Z",
"git_url": "git://github.com/baxterandthehackers/public-repo.git",
"ssh_url": "git@github.com:baxterandthehackers/public-repo.git",
"clone_url": "https://github.com/baxterandthehackers/public-repo.git",
"svn_url": "https://github.com/baxterandthehackers/public-repo",
"homepage": null,
"size": 0,
"stargazers_count": 0,
"watchers_count": 0,
"language": null,
"has_issues": true,
"has_downloads": true,
"has_wiki": true,
"has_pages": false,
"forks_count": 0,
"mirror_url": null,
"open_issues_count": 0,
"forks": 0,
"open_issues": 0,
"watchers": 0,
"default_branch": "master"
},
"organization": {
"login": "baxterandthehackers",
"id": 7649605,
"url": "https://api.github.com/orgs/baxterandthehackers",
"repos_url": "https://api.github.com/orgs/baxterandthehackers/repos",
"events_url": "https://api.github.com/orgs/baxterandthehackers/events",
"members_url": "https://api.github.com/orgs/baxterandthehackers/members{/member}",
"public_members_url": "https://api.github.com/orgs/baxterandthehackers/public_members{/member}",
"avatar_url": "https://avatars.githubusercontent.com/u/7649605?v=2"
},
"sender": {
"login": "baxterthehacker",
"id": 6752317,
"avatar_url": "https://avatars.githubusercontent.com/u/6752317?v=2",
"gravatar_id": "",
"url": "https://api.github.com/users/baxterthehacker",
"html_url": "https://github.com/baxterthehacker",
"followers_url": "https://api.github.com/users/baxterthehacker/followers",
"following_url": "https://api.github.com/users/baxterthehacker/following{/other_user}",
"gists_url": "https://api.github.com/users/baxterthehacker/gists{/gist_id}",
"starred_url": "https://api.github.com/users/baxterthehacker/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/baxterthehacker/subscriptions",
"organizations_url": "https://api.github.com/users/baxterthehacker/orgs",
"repos_url": "https://api.github.com/users/baxterthehacker/repos",
"events_url": "https://api.github.com/users/baxterthehacker/events{/privacy}",
"received_events_url": "https://api.github.com/users/baxterthehacker/received_events",
"type": "User",
"site_admin": false
}
}

View File

@ -61,6 +61,15 @@ class GitHubWebhookTest(WebhookTestCase):
expected_message = "baxterthehacker [pushed](https://github.com/baxterthehacker/public-repo/compare/9049f1265b7d...0d1a26e67d8f) 1 commit to branch changes.\n\n* Update README.md ([0d1a26e67d8](https://github.com/baxterthehacker/public-repo/commit/0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c))" expected_message = "baxterthehacker [pushed](https://github.com/baxterthehacker/public-repo/compare/9049f1265b7d...0d1a26e67d8f) 1 commit to branch changes.\n\n* Update README.md ([0d1a26e67d8](https://github.com/baxterthehacker/public-repo/commit/0d1a26e67d8f5eaf1f6ba5c57fc3c7d91ac0fd1c))"
self.check_webhook("push__1_commit", TOPIC_BRANCH, expected_message) self.check_webhook("push__1_commit", TOPIC_BRANCH, expected_message)
def test_push_1_commit_private_repository_skipped(self) -> None:
self.url = self.build_webhook_url(ignore_private_repositories="true")
self.check_webhook(
fixture_name="push__1_commit_private_repository",
expected_topic_name=None,
expected_message=None,
expect_noop=True,
)
def test_push_1_commit_without_username(self) -> None: def test_push_1_commit_without_username(self) -> None:
expected_message = "eeshangarg [pushed](https://github.com/eeshangarg/public-repo/compare/0383613da871...2e8cf535fb38) 1 commit to branch changes. Commits by John Snow (1).\n\n* Update the README ([2e8cf535fb3](https://github.com/eeshangarg/public-repo/commit/2e8cf535fb38a3dab2476cdf856efda904ad4c94))" expected_message = "eeshangarg [pushed](https://github.com/eeshangarg/public-repo/compare/0383613da871...2e8cf535fb38) 1 commit to branch changes. Commits by John Snow (1).\n\n* Update the README ([2e8cf535fb3](https://github.com/eeshangarg/public-repo/commit/2e8cf535fb38a3dab2476cdf856efda904ad4c94))"
self.check_webhook("push__1_commit_without_username", TOPIC_BRANCH, expected_message) self.check_webhook("push__1_commit_without_username", TOPIC_BRANCH, expected_message)
@ -268,6 +277,15 @@ class GitHubWebhookTest(WebhookTestCase):
) )
self.check_webhook("pull_request__merged", TOPIC_PR, expected_message) self.check_webhook("pull_request__merged", TOPIC_PR, expected_message)
def test_pull_request_merged_msg_private_repository_skipped(self) -> None:
self.url = self.build_webhook_url(ignore_private_repositories="true")
self.check_webhook(
fixture_name="pull_request__merged_private_repository",
expected_topic_name=None,
expected_message=None,
expect_noop=True,
)
def test_public_msg(self) -> None: def test_public_msg(self) -> None:
expected_message = "baxterthehacker made the repository [baxterthehacker/public-repo](https://github.com/baxterthehacker/public-repo) public." expected_message = "baxterthehacker made the repository [baxterthehacker/public-repo](https://github.com/baxterthehacker/public-repo) public."
self.check_webhook("public", TOPIC_REPO, expected_message) self.check_webhook("public", TOPIC_REPO, expected_message)
@ -284,6 +302,19 @@ class GitHubWebhookTest(WebhookTestCase):
expected_message = "baxterthehacker created the repository [baxterandthehackers/public-repo](https://github.com/baxterandthehackers/public-repo)." expected_message = "baxterthehacker created the repository [baxterandthehackers/public-repo](https://github.com/baxterandthehackers/public-repo)."
self.check_webhook("repository", TOPIC_REPO, expected_message) self.check_webhook("repository", TOPIC_REPO, expected_message)
def test_private_repository_msg(self) -> None:
expected_message = "baxterthehacker created the repository [baxterandthehackers/public-repo](https://github.com/baxterandthehackers/public-repo)."
self.check_webhook("repository", TOPIC_REPO, expected_message)
def test_private_repository_skipped_msg(self) -> None:
self.url = self.build_webhook_url(ignore_private_repositories="true")
self.check_webhook(
fixture_name="repository_private",
expected_topic_name=None,
expected_message=None,
expect_noop=True,
)
def test_team_add_msg(self) -> None: def test_team_add_msg(self) -> None:
expected_message = "The repository [baxterandthehackers/public-repo](https://github.com/baxterandthehackers/public-repo) was added to team github." expected_message = "The repository [baxterandthehackers/public-repo](https://github.com/baxterandthehackers/public-repo) was added to team github."
self.check_webhook("team_add", TOPIC_REPO, expected_message) self.check_webhook("team_add", TOPIC_REPO, expected_message)

View File

@ -3,6 +3,7 @@ from collections.abc import Callable
from datetime import datetime, timezone from datetime import datetime, timezone
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse
from pydantic import Json
from zerver.decorator import log_unsupported_webhook_event, webhook_view from zerver.decorator import log_unsupported_webhook_event, webhook_view
from zerver.lib.exceptions import UnsupportedWebhookEventTypeError from zerver.lib.exceptions import UnsupportedWebhookEventTypeError
@ -899,6 +900,7 @@ def api_github_webhook(
payload: JsonBodyPayload[WildValue], payload: JsonBodyPayload[WildValue],
branches: str | None = None, branches: str | None = None,
user_specified_topic: OptionalUserSpecifiedTopicStr = None, user_specified_topic: OptionalUserSpecifiedTopicStr = None,
ignore_private_repositories: Json[bool] = False,
) -> HttpResponse: ) -> HttpResponse:
""" """
GitHub sends the event as an HTTP header. We have our GitHub sends the event as an HTTP header. We have our
@ -908,6 +910,15 @@ def api_github_webhook(
""" """
header_event = validate_extract_webhook_http_header(request, "X-GitHub-Event", "GitHub") header_event = validate_extract_webhook_http_header(request, "X-GitHub-Event", "GitHub")
# Check if the repository is private and skip processing if ignore_private_repositories is True
if (
"repository" in payload
and payload["repository"]["private"].tame(check_bool)
and ignore_private_repositories
):
# Ignore private repository events
return json_success(request)
event = get_zulip_event_name(header_event, payload, branches) event = get_zulip_event_name(header_event, payload, branches)
if event is None: if event is None:
# This is nothing to worry about--get_event() returns None # This is nothing to worry about--get_event() returns None