= comments.iter().map(|c| c.id).collect();
+ assert_eq!(ids, vec![1, 2]);
+ }
+
+ #[test]
+ #[cfg(unix)]
+ fn upsert_marked_pr_comment_edits_the_users_existing_comment() {
+ let bin_dir = TempDir::new().unwrap();
+ let body_path = bin_dir.path().join("body");
+ let gh_path = write_fake_gh(
+ &bin_dir,
+ &format!(
+ r#"case "$*" in
+ "{COMMENTS_ON_5}")
+ echo '[{{"id":11,"body":"old {STACK_MARKER}","user":{{"login":"bob"}}}},{{"id":12,"body":"old {STACK_MARKER}","user":{{"login":"Alice"}}}}]' ;;
+ "api --method PATCH repos/owner/repo/issues/comments/12 --input -")
+ cat > '{}' ;;
+ *) exit 9 ;;
+esac"#,
+ body_path.display()
+ ),
+ );
+ let body = format!("new {STACK_MARKER}");
+
+ let write = upsert_marked_pr_comment_impl(
+ &gh_path,
+ "owner/repo",
+ 5,
+ "alice",
+ STACK_MARKER,
+ &body,
+ "/usr/bin:/bin",
+ )
+ .unwrap();
+
+ assert_eq!(write, CommentWrite::Updated);
+ let sent: serde_json::Value =
+ serde_json::from_str(&fs::read_to_string(&body_path).unwrap()).unwrap();
+ assert_eq!(sent, serde_json::json!({ "body": body }));
+ }
+
+ #[test]
+ #[cfg(unix)]
+ fn upsert_marked_pr_comment_posts_when_the_user_has_no_marked_comment() {
+ let bin_dir = TempDir::new().unwrap();
+ let body_path = bin_dir.path().join("body");
+ let gh_path = write_fake_gh(
+ &bin_dir,
+ &format!(
+ r#"case "$*" in
+ "{COMMENTS_ON_5}")
+ echo '[{{"id":11,"body":"{STACK_MARKER}","user":{{"login":"bob"}}}},{{"id":12,"body":"LGTM","user":{{"login":"alice"}}}}]' ;;
+ "pr comment 5 --repo owner/repo --body-file -")
+ cat > '{}' ;;
+ *) exit 9 ;;
+esac"#,
+ body_path.display()
+ ),
+ );
+ let body = format!("new {STACK_MARKER}");
+
+ let write = upsert_marked_pr_comment_impl(
+ &gh_path,
+ "owner/repo",
+ 5,
+ "alice",
+ STACK_MARKER,
+ &body,
+ "/usr/bin:/bin",
+ )
+ .unwrap();
+
+ assert_eq!(write, CommentWrite::Created);
+ assert_eq!(fs::read_to_string(&body_path).unwrap(), body);
+ }
+
+ #[test]
+ #[cfg(unix)]
+ fn upsert_marked_pr_comment_leaves_an_unchanged_comment_alone() {
+ let bin_dir = TempDir::new().unwrap();
+ let gh_path = write_fake_gh(
+ &bin_dir,
+ &format!(
+ r#"test "$*" = "{COMMENTS_ON_5}" || exit 9
+echo '[{{"id":12,"body":"same {STACK_MARKER}","user":{{"login":"alice"}}}}]'"#
+ ),
+ );
+
+ let write = upsert_marked_pr_comment_impl(
+ &gh_path,
+ "owner/repo",
+ 5,
+ "alice",
+ STACK_MARKER,
+ &format!("same {STACK_MARKER}"),
+ "/usr/bin:/bin",
+ )
+ .unwrap();
+
+ assert_eq!(write, CommentWrite::Unchanged);
+ }
+
+ #[test]
+ #[cfg(unix)]
+ fn upsert_marked_pr_comment_rejects_a_malformed_repo_without_running_gh() {
+ let bin_dir = TempDir::new().unwrap();
+ let ran = bin_dir.path().join("ran");
+ let gh_path = write_fake_gh(&bin_dir, &format!("touch '{}'", ran.display()));
+
+ let result = upsert_marked_pr_comment_impl(
+ &gh_path,
+ "owner/repo/../../user",
+ 5,
+ "alice",
+ STACK_MARKER,
+ STACK_MARKER,
+ "/usr/bin:/bin",
+ );
+
+ assert!(result.is_err());
+ assert!(!ran.exists());
+ }
}
diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs
index ca6860118..a54074431 100644
--- a/src-tauri/src/lib.rs
+++ b/src-tauri/src/lib.rs
@@ -908,6 +908,7 @@ pub fn run() {
commands::gh_reopen_pr,
commands::gh_set_pr_draft,
commands::gh_create_pr,
+ commands::gh_sync_stack_comments,
commands::gh_list_pr_review_threads,
commands::linear_list_teams,
commands::linear_list_issues,
diff --git a/src/components/RepositorySettingsContent.tsx b/src/components/RepositorySettingsContent.tsx
index b180cf53c..8d49130bb 100644
--- a/src/components/RepositorySettingsContent.tsx
+++ b/src/components/RepositorySettingsContent.tsx
@@ -51,6 +51,7 @@ export const RepositorySettingsContent = ({
defaultAgent: string;
autoPush: boolean;
ignoreGeneratedAgentFiles: boolean;
+ postStackComments: boolean;
reviewPrompt: string;
reviewAgent: string;
autoReviewTrigger: string;
@@ -68,6 +69,7 @@ export const RepositorySettingsContent = ({
agent,
autoPushSetting,
ignoreGeneratedSetting,
+ postStackCommentsSetting,
reviewPromptSetting,
reviewAgentSetting,
autoReviewTriggerSetting,
@@ -78,6 +80,7 @@ export const RepositorySettingsContent = ({
getRepoSetting(repoPath, "default_agent"),
getRepoSetting(repoPath, "auto_push"),
getRepoSetting(repoPath, "ignore_generated_treq_paths"),
+ getRepoSetting(repoPath, "post_stack_comments"),
getRepoSetting(repoPath, "review_prompt"),
getRepoSetting(repoPath, "review_agent"),
getRepoSetting(repoPath, "auto_review_trigger"),
@@ -89,6 +92,7 @@ export const RepositorySettingsContent = ({
defaultAgent: agent || "",
autoPush: autoPushSetting === "true",
ignoreGeneratedAgentFiles: ignoreGeneratedSetting === "true",
+ postStackComments: postStackCommentsSetting !== "false",
reviewPrompt: reviewPromptSetting || "",
reviewAgent: reviewAgentSetting || "",
autoReviewTrigger: normalizeAutoReviewTrigger(autoReviewTriggerSetting),
@@ -105,6 +109,7 @@ export const RepositorySettingsContent = ({
defaultAgent: "",
autoPush: false,
ignoreGeneratedAgentFiles: false,
+ postStackComments: true,
reviewPrompt: "",
reviewAgent: "",
autoReviewTrigger: "off",
@@ -116,6 +121,7 @@ export const RepositorySettingsContent = ({
defaultAgent,
autoPush,
ignoreGeneratedAgentFiles,
+ postStackComments,
reviewPrompt,
reviewAgent,
autoReviewTrigger,
@@ -145,6 +151,7 @@ export const RepositorySettingsContent = ({
defaultAgent: string;
autoPush: boolean;
ignoreGeneratedAgentFiles: boolean;
+ postStackComments: boolean;
reviewPrompt: string;
reviewAgent: string;
autoReviewTrigger: string;
@@ -158,6 +165,7 @@ export const RepositorySettingsContent = ({
defaultAgent,
autoPush,
ignoreGeneratedAgentFiles,
+ postStackComments,
reviewPrompt,
reviewAgent,
autoReviewTrigger,
@@ -173,6 +181,8 @@ export const RepositorySettingsContent = ({
const setAutoPush = (v: boolean) => updateDraft({ autoPush: v });
const setIgnoreGeneratedAgentFiles = (v: boolean) =>
updateDraft({ ignoreGeneratedAgentFiles: v });
+ const setPostStackComments = (v: boolean) =>
+ updateDraft({ postStackComments: v });
const setReviewPrompt = (v: string) => updateDraft({ reviewPrompt: v });
const setReviewAgent = (v: string) => updateDraft({ reviewAgent: v });
const setAutoReviewTrigger = (v: string) =>
@@ -193,6 +203,11 @@ export const RepositorySettingsContent = ({
"ignore_generated_treq_paths",
ignoreGeneratedAgentFiles ? "true" : "false",
),
+ setRepoSetting(
+ repoPath,
+ "post_stack_comments",
+ postStackComments ? "true" : "false",
+ ),
setRepoSetting(repoPath, "review_prompt", reviewPrompt),
setRepoSetting(repoPath, "review_agent", reviewAgent),
setRepoSetting(repoPath, "auto_review_trigger", autoReviewTrigger),
@@ -332,6 +347,24 @@ export const RepositorySettingsContent = ({
/>
+
+
+
+
+ When Treq creates a stacked pull request, it comments on each open
+ pull request in the stack with the merge order. Everyone who can see
+ the pull request on GitHub sees the comment.
+
+
+
+
+
("ready");
const draft = prType === "draft";
+ const addToast = useToastStore((s) => s.addToast);
const { data: defaultBranch } = useSWR(
repoPath ? ["repo-default-branch", repoPath] : null,
@@ -129,6 +132,10 @@ export function CreatePrForm({
void invalidateQueries(["gh-prs", repoFullName]);
void invalidatePrStatuses(repoPath, head);
onSuccess(prNumber);
+ void syncStackCommentsAfterPrCreate(
+ { repoPath, repoFullName, headBranch: head },
+ addToast,
+ );
},
});
diff --git a/src/hooks/useCreateWorkspacePr.ts b/src/hooks/useCreateWorkspacePr.ts
index 6599983f3..f2098003f 100644
--- a/src/hooks/useCreateWorkspacePr.ts
+++ b/src/hooks/useCreateWorkspacePr.ts
@@ -1,5 +1,6 @@
import { openUrl } from "@tauri-apps/plugin-opener";
import { ghCreatePr } from "../lib/api";
+import { syncStackCommentsAfterPrCreate } from "../lib/stack-comments";
import { invalidateQueries } from "../lib/swr-cache";
import { useToastStore } from "../stores/toastStore";
import {
@@ -73,6 +74,16 @@ export function useCreateWorkspacePr(
onClick: () => openUrl(prUrl),
},
});
+ if (repoPath) {
+ void syncStackCommentsAfterPrCreate(
+ {
+ repoPath,
+ repoFullName: request.repoFullName,
+ headBranch: request.branchName,
+ },
+ addToast,
+ );
+ }
return number;
} catch (error) {
addToast({
diff --git a/src/lib/api-github.ts b/src/lib/api-github.ts
index 8a7c4cfa1..d8037deea 100644
--- a/src/lib/api-github.ts
+++ b/src/lib/api-github.ts
@@ -3,6 +3,7 @@ import type {
GhListPage,
GhPullRequest,
GhReviewThread,
+ StackCommentOutcome,
} from "./api-types";
import { invoke } from "@tauri-apps/api/core";
@@ -140,3 +141,11 @@ export const ghCreatePr = (
headBranch,
draft,
});
+
+/** Post or refresh the stack comment on every open PR in `headBranch`'s stack. */
+export const ghSyncStackComments = (
+ repoPath: string,
+ repoFullName: string,
+ headBranch: string,
+): Promise =>
+ invoke("gh_sync_stack_comments", { repoPath, repoFullName, headBranch });
diff --git a/src/lib/api-types-github.ts b/src/lib/api-types-github.ts
index 49e936f1e..d648a3ebf 100644
--- a/src/lib/api-types-github.ts
+++ b/src/lib/api-types-github.ts
@@ -73,3 +73,9 @@ export interface GhReviewThread {
diff_side: string;
comments: GhReviewComment[];
}
+
+/** What `gh_sync_stack_comments` did after a PR was created. */
+export type StackCommentOutcome =
+ | { status: "disabled" }
+ | { status: "not_stacked" }
+ | { status: "posted"; created: number; updated: number; unchanged: number };
diff --git a/src/lib/stack-comments.ts b/src/lib/stack-comments.ts
new file mode 100644
index 000000000..4371f8ac1
--- /dev/null
+++ b/src/lib/stack-comments.ts
@@ -0,0 +1,28 @@
+import type { ToastState } from "../stores/toastStore";
+import { ghSyncStackComments } from "./api";
+
+export interface CreatedPr {
+ repoPath: string;
+ repoFullName: string;
+ headBranch: string;
+}
+
+/**
+ * Posts or refreshes the stack comment on a newly created PR and the rest of
+ * its stack. The PR already exists by then, so a failure only shows a
+ * warning toast.
+ */
+export async function syncStackCommentsAfterPrCreate(
+ { repoPath, repoFullName, headBranch }: CreatedPr,
+ addToast: ToastState["addToast"],
+): Promise {
+ try {
+ await ghSyncStackComments(repoPath, repoFullName, headBranch);
+ } catch (error) {
+ addToast({
+ title: "Stack comment not updated",
+ description: error instanceof Error ? error.message : String(error),
+ type: "warning",
+ });
+ }
+}
diff --git a/test/integration/github-panel-detail.test.tsx b/test/integration/github-panel-detail.test.tsx
index ba446b533..4d4e842ce 100644
--- a/test/integration/github-panel-detail.test.tsx
+++ b/test/integration/github-panel-detail.test.tsx
@@ -19,6 +19,7 @@ const api = vi.hoisted(() => ({
getPrInfoViaGh: vi.fn(),
getPrChecksViaGh: vi.fn(),
ghCreatePr: vi.fn(),
+ ghSyncStackComments: vi.fn(),
getWorkspaces: vi.fn(),
openOrCreateWorkspaceFromPr: vi.fn(),
}));
@@ -38,6 +39,7 @@ vi.mock("../../src/lib/api", async (importOriginal) => {
getPrInfoViaGh: api.getPrInfoViaGh,
getPrChecksViaGh: api.getPrChecksViaGh,
ghCreatePr: api.ghCreatePr,
+ ghSyncStackComments: api.ghSyncStackComments,
getWorkspaces: api.getWorkspaces,
openOrCreateWorkspaceFromPr: api.openOrCreateWorkspaceFromPr,
};
@@ -423,6 +425,9 @@ describe("CreatePrForm", () => {
beforeEach(() => {
user = userEvent.setup();
api.ghCreatePr.mockReset().mockResolvedValue(7);
+ api.ghSyncStackComments
+ .mockReset()
+ .mockResolvedValue({ status: "not_stacked" });
});
async function fillForm() {
@@ -499,6 +504,30 @@ describe("CreatePrForm", () => {
expect(onSuccess).toHaveBeenCalledWith(7);
});
+ it("updates the stack comment for the new PR's head branch", async () => {
+ render(
+ {}}
+ onCancel={() => {}}
+ />,
+ );
+
+ await fillForm();
+ await user.click(
+ screen.getByRole("button", { name: /^create pull request$/i }),
+ );
+
+ await waitFor(() => {
+ expect(api.ghSyncStackComments).toHaveBeenCalledWith(
+ "/tmp/repo",
+ "acme/treq",
+ "feat/thing",
+ );
+ });
+ });
+
it("shows the gh error message without an Error prefix", async () => {
api.ghCreatePr.mockRejectedValue(new Error("gh: head branch not found"));
render(
diff --git a/test/integration/settings.test.tsx b/test/integration/settings.test.tsx
index 504046587..bb30f80f2 100644
--- a/test/integration/settings.test.tsx
+++ b/test/integration/settings.test.tsx
@@ -4,13 +4,15 @@ import { beforeEach, describe, expect, it } from "vitest";
import { createTestRepo, openRepo } from "../utils";
import { render, screen } from "../test-utils";
import { Dashboard } from "../../src/components/Dashboard";
+import { getRepoSetting } from "../../src/lib/api";
import userEvent from "@testing-library/user-event";
describe("Settings integration", () => {
let user: ReturnType;
+ let repoPath: string;
beforeEach(() => {
- const { repoPath } = createTestRepo(false);
+ ({ repoPath } = createTestRepo(false));
openRepo(repoPath);
user = userEvent.setup();
});
@@ -61,6 +63,29 @@ describe("Settings integration", () => {
expect(screen.queryByLabelText(/theme/i)).not.toBeInTheDocument();
});
+ it("posts stack comments by default and saves the setting when turned off", async () => {
+ render();
+
+ await user.click(await screen.findByLabelText("Settings"));
+ const toggle = await screen.findByRole("switch", {
+ name: /post stack comments on pull requests/i,
+ });
+ expect(toggle).toHaveAttribute("aria-checked", "true");
+
+ await user.click(toggle);
+ await user.click(screen.getByRole("button", { name: /save settings/i }));
+ await screen.findByText("Settings saved");
+ expect(await getRepoSetting(repoPath, "post_stack_comments")).toBe("false");
+
+ await user.click(screen.getByRole("button", { name: "Close" }));
+ await user.click(await screen.findByLabelText("Settings"));
+ expect(
+ await screen.findByRole("switch", {
+ name: /post stack comments on pull requests/i,
+ }),
+ ).toHaveAttribute("aria-checked", "false");
+ });
+
it("returns to the previous page when settings is closed", async () => {
render();
diff --git a/test/integration/stack-comments.test.ts b/test/integration/stack-comments.test.ts
new file mode 100644
index 000000000..954afe76b
--- /dev/null
+++ b/test/integration/stack-comments.test.ts
@@ -0,0 +1,30 @@
+// @include-parallel
+import { describe, expect, it } from "vitest";
+import {
+ createWorkspace,
+ ghSyncStackComments,
+ setRepoSetting,
+} from "../../src/lib/api";
+import { createTestRepo } from "../utils";
+
+describe("gh_sync_stack_comments", () => {
+ it("leaves a workspace outside any stack alone", async () => {
+ const { repoPath } = createTestRepo(false);
+ await createWorkspace(repoPath, "feat/solo");
+
+ await expect(
+ ghSyncStackComments(repoPath, "acme/treq", "feat/solo"),
+ ).resolves.toEqual({ status: "not_stacked" });
+ });
+
+ it("posts nothing for a stack when the repository setting is off", async () => {
+ const { repoPath } = createTestRepo(false);
+ await createWorkspace(repoPath, "feat/parent");
+ await createWorkspace(repoPath, "feat/child", "feat/parent");
+ await setRepoSetting(repoPath, "post_stack_comments", "false");
+
+ await expect(
+ ghSyncStackComments(repoPath, "acme/treq", "feat/child"),
+ ).resolves.toEqual({ status: "disabled" });
+ });
+});
diff --git a/test/integration/workspace/create-pr.test.tsx b/test/integration/workspace/create-pr.test.tsx
index c7c23fca1..92c6b6f5f 100644
--- a/test/integration/workspace/create-pr.test.tsx
+++ b/test/integration/workspace/create-pr.test.tsx
@@ -11,6 +11,7 @@ import {
ghCreatePr,
getPrInfoViaGh,
getWorkspaceStatus,
+ ghSyncStackComments,
pushWorkspaceToRemote,
updateWorkspace,
} from "../../../src/lib/api";
@@ -40,6 +41,7 @@ vi.mock("../../../src/lib/api", async (importOriginal) => {
refreshPrStatuses: vi.fn(async () => undefined),
getPrChecksForPr: vi.fn().mockResolvedValue(null),
ghCreatePr: vi.fn().mockResolvedValue(42),
+ ghSyncStackComments: vi.fn(),
ghListPrs: vi.fn().mockResolvedValue({ items: [], hasMore: false }),
ghListIssues: vi.fn().mockResolvedValue({ items: [], hasMore: false }),
pushWorkspaceToRemote: vi.fn(
@@ -65,6 +67,9 @@ describe("ShowWorkspace - Create PR", () => {
vi.mocked(getCachedPrInfo).mockReset().mockResolvedValue(null);
vi.mocked(getPrInfoViaGh).mockReset().mockResolvedValue(null);
vi.mocked(ghCreatePr).mockReset().mockResolvedValue(42);
+ vi.mocked(ghSyncStackComments)
+ .mockReset()
+ .mockResolvedValue({ status: "not_stacked" });
vi.mocked(openUrl).mockReset();
});
@@ -341,6 +346,42 @@ describe("ShowWorkspace - Create PR", () => {
).not.toBeInTheDocument();
});
+ it("updates the stack comment after the PR is created", async () => {
+ await setupPushedWorkspaceWithGitHub();
+ render();
+
+ const header = await openWorkspace("feat/create-pr");
+ await user.click(await findEnabledCreatePr(header));
+
+ expect(await screen.findByText("Pull request created")).toBeVisible();
+ await waitFor(() => {
+ expect(ghSyncStackComments).toHaveBeenCalledWith(
+ repoPath,
+ "acme/treq",
+ "feat/create-pr",
+ );
+ });
+ expect(vi.mocked(ghCreatePr).mock.invocationCallOrder[0]).toBeLessThan(
+ vi.mocked(ghSyncStackComments).mock.invocationCallOrder[0],
+ );
+ });
+
+ it("keeps the created PR when the stack comment fails", async () => {
+ vi.mocked(ghSyncStackComments).mockRejectedValue(
+ new Error("gh exited with error: HTTP 403"),
+ );
+ await setupPushedWorkspaceWithGitHub();
+ render();
+
+ const header = await openWorkspace("feat/create-pr");
+ await user.click(await findEnabledCreatePr(header));
+
+ expect(await screen.findByText("Pull request created")).toBeVisible();
+ expect(await screen.findByText("Stack comment not updated")).toBeVisible();
+ expect(screen.getByText("gh exited with error: HTTP 403")).toBeVisible();
+ expect(screen.queryByText("Failed to create PR")).toBeNull();
+ });
+
it("creates a PR with the workspace title and description", async () => {
const { title, description } = await setupPushedWorkspaceWithGitHub();
render();
diff --git a/web/docs/concepts/github-integration.mdx b/web/docs/concepts/github-integration.mdx
index 0d6c4d139..a59f80580 100644
--- a/web/docs/concepts/github-integration.mdx
+++ b/web/docs/concepts/github-integration.mdx
@@ -40,6 +40,24 @@ The Review tab Commit control can **Commit and create PR** or **Commit and push*
For the full create and view flow, see [Creating and Viewing Pull Requests](/docs/how-to/creating-and-viewing-pull-requests).
+## Stack Comments
+
+A workspace belongs to a stack when its target is another workspace, or when another workspace targets it. See [Workspaces](/docs/concepts/workspaces). When Treq creates a pull request for a stacked workspace, it comments on each open pull request in that stack. The comment lists the stack newest first, marks the pull request it sits on, and ends with the base branch:
+
+```markdown
+**Stack** (merge from the bottom up)
+- #103 Add tests
+- **#102 Refactor parser** ← this PR
+- #101 Extract module
+- `main`
+```
+
+A short footer links to treq.dev. Treq posts the comment through your `gh` login, so it appears under your GitHub account. Every reviewer who can open the pull request can read it, including people who do not use Treq.
+
+Each pull request keeps one stack comment. When you create another pull request in the stack, Treq edits the existing comments instead of adding new ones. Treq posts nothing when only one pull request in the stack is open. If a comment fails to post, the pull request still exists and Treq shows a warning.
+
+Stack comments are on by default. To turn them off for a repository, open Settings, switch off **Post stack comments on pull requests** on the Repository tab, and click **Save Settings**. With the setting off, Treq neither posts nor edits stack comments in that repository.
+
## GitHub Panel
Open **GitHub** in the workspace sidebar to reach Issues and Pull Requests. The panel uses hash routes such as `#/github/prs/open` so the `?repo=` query stays intact.
diff --git a/web/docs/how-to/customizing-settings.md b/web/docs/how-to/customizing-settings.md
index c97a78b1d..9f68bc9d9 100644
--- a/web/docs/how-to/customizing-settings.md
+++ b/web/docs/how-to/customizing-settings.md
@@ -26,6 +26,8 @@ Release builds start from the shipped default. Dev builds start with every previ
**Auto-push to remote** pushes after every commit in this repository when enabled. It is off by default. Turn it on when you want each commit on the remote without a separate push step. Create PR still pushes on its own when the branch is missing remotely.
+**Post stack comments on pull requests** is on by default. When Treq creates a pull request for a stacked workspace, it comments on each open pull request in the stack. Switch it off to stop posting and editing those comments. See [Stack Comments](/docs/concepts/github-integration#stack-comments).
+
**Branch naming pattern** builds branch names from variables.
| Variable | Value |
diff --git a/web/docs/security-and-privacy.md b/web/docs/security-and-privacy.md
index 4dfe1e9bb..24c696550 100644
--- a/web/docs/security-and-privacy.md
+++ b/web/docs/security-and-privacy.md
@@ -55,3 +55,7 @@ Audit the code yourself, or follow the public history of changes. These files ar
- [Connecting GitHub](/docs/how-to/connecting-github)
- [CLI](/docs/reference/cli)
- [Contributing](/docs/reference/contributing)
+
+## Stack Comments on GitHub
+
+When you create a pull request for a stacked workspace, Treq posts a comment on each open pull request in the stack through your `gh` login. The comment lists the stack's pull requests and links to treq.dev, and anyone who can open those pull requests can read it. To stop it, switch off **Post stack comments on pull requests** in the repository's Settings. See [Stack Comments](/docs/concepts/github-integration#stack-comments).