Skip to content

Commit cce9a18

Browse files
committed
Add clickable branch links and support for remote branch/PR checkout
- Branch names in UI are now clickable and link to GitHub - Added --remote-branch option to checkout remote branches - Added --pr option to checkout specific PRs by number - Fixed shadow repo sync to preserve git-tracked files regardless of gitignore - Updated documentation with shadow repo sync principles
1 parent 0703113 commit cce9a18

File tree

5 files changed

+121
-14
lines changed

5 files changed

+121
-14
lines changed

public/app.js

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -665,7 +665,30 @@ function updateGitInfo(data) {
665665
const prInfoElement = document.getElementById("pr-info");
666666

667667
if (data.currentBranch) {
668-
branchNameElement.textContent = data.currentBranch;
668+
// Clear existing content
669+
branchNameElement.innerHTML = "";
670+
671+
if (data.branchUrl) {
672+
// Create clickable branch link
673+
const branchLink = document.createElement("a");
674+
branchLink.href = data.branchUrl;
675+
branchLink.target = "_blank";
676+
branchLink.textContent = data.currentBranch;
677+
branchLink.style.color = "inherit";
678+
branchLink.style.textDecoration = "none";
679+
branchLink.title = `View ${data.currentBranch} branch on GitHub`;
680+
branchLink.addEventListener("mouseenter", () => {
681+
branchLink.style.textDecoration = "underline";
682+
});
683+
branchLink.addEventListener("mouseleave", () => {
684+
branchLink.style.textDecoration = "none";
685+
});
686+
branchNameElement.appendChild(branchLink);
687+
} else {
688+
// Fallback to plain text
689+
branchNameElement.textContent = data.currentBranch;
690+
}
691+
669692
gitInfoElement.style.display = "inline-block";
670693
}
671694

src/cli.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ program
8080
)
8181
.option("-n, --name <name>", "Container name prefix")
8282
.option("--no-push", "Disable automatic branch pushing")
83-
.option("--no-pr", "Disable automatic PR creation")
83+
.option("--no-create-pr", "Disable automatic PR creation")
8484
.option(
8585
"--include-untracked",
8686
"Include untracked files when copying to container",
@@ -89,6 +89,14 @@ program
8989
"-b, --branch <branch>",
9090
"Switch to specific branch on container start (creates if doesn't exist)",
9191
)
92+
.option(
93+
"--remote-branch <branch>",
94+
"Checkout a remote branch (e.g., origin/feature-branch)",
95+
)
96+
.option(
97+
"--pr <number>",
98+
"Checkout a specific PR by number",
99+
)
92100
.option(
93101
"--shell <shell>",
94102
"Start with 'claude' or 'bash' shell",
@@ -100,9 +108,11 @@ program
100108
const config = await loadConfig(options.config);
101109
config.containerPrefix = options.name || config.containerPrefix;
102110
config.autoPush = options.push !== false;
103-
config.autoCreatePR = options.pr !== false;
111+
config.autoCreatePR = options.createPr !== false;
104112
config.includeUntracked = options.includeUntracked || false;
105113
config.targetBranch = options.branch;
114+
config.remoteBranch = options.remoteBranch;
115+
config.prNumber = options.pr;
106116
if (options.shell) {
107117
config.defaultShell = options.shell.toLowerCase();
108118
}

src/index.ts

Lines changed: 61 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -38,17 +38,67 @@ export class ClaudeSandbox {
3838
const currentBranch = await this.git.branchLocal();
3939
console.log(chalk.blue(`Current branch: ${currentBranch.current}`));
4040

41-
// Use target branch from config or generate one
42-
const branchName =
43-
this.config.targetBranch ||
44-
(() => {
45-
const timestamp = new Date()
46-
.toISOString()
47-
.replace(/[:.]/g, "-")
48-
.split("T")[0];
49-
return `claude/${timestamp}-${Date.now()}`;
50-
})();
51-
console.log(chalk.blue(`Will create branch in container: ${branchName}`));
41+
// Determine target branch based on config options
42+
let branchName = "";
43+
44+
if (this.config.prNumber) {
45+
// Checkout PR
46+
console.log(chalk.blue(`Fetching PR #${this.config.prNumber}...`));
47+
try {
48+
branchName = `pr-${this.config.prNumber}`;
49+
50+
// Check if PR branch already exists locally
51+
const branches = await this.git.branchLocal();
52+
if (branches.all.includes(branchName)) {
53+
// PR branch exists, just checkout
54+
await this.git.checkout(branchName);
55+
console.log(chalk.green(`✓ Switched to existing PR branch: ${branchName}`));
56+
} else {
57+
// Fetch and create new PR branch
58+
await this.git.fetch("origin", `pull/${this.config.prNumber}/head:${branchName}`);
59+
await this.git.checkout(branchName);
60+
console.log(chalk.green(`✓ Checked out PR #${this.config.prNumber}`));
61+
}
62+
} catch (error) {
63+
console.error(chalk.red(`✗ Failed to checkout PR #${this.config.prNumber}:`), error);
64+
throw error;
65+
}
66+
} else if (this.config.remoteBranch) {
67+
// Checkout remote branch
68+
console.log(chalk.blue(`Checking out remote branch: ${this.config.remoteBranch}...`));
69+
try {
70+
await this.git.fetch("origin");
71+
const localBranchName = this.config.remoteBranch.replace("origin/", "");
72+
73+
// Check if local branch already exists
74+
const branches = await this.git.branchLocal();
75+
if (branches.all.includes(localBranchName)) {
76+
// Local branch exists, just checkout
77+
await this.git.checkout(localBranchName);
78+
console.log(chalk.green(`✓ Switched to existing branch: ${localBranchName}`));
79+
} else {
80+
// Create new local branch from remote
81+
await this.git.checkoutBranch(localBranchName, this.config.remoteBranch);
82+
console.log(chalk.green(`✓ Checked out remote branch: ${this.config.remoteBranch}`));
83+
}
84+
branchName = localBranchName;
85+
} catch (error) {
86+
console.error(chalk.red(`✗ Failed to checkout remote branch ${this.config.remoteBranch}:`), error);
87+
throw error;
88+
}
89+
} else {
90+
// Use target branch from config or generate one
91+
branchName =
92+
this.config.targetBranch ||
93+
(() => {
94+
const timestamp = new Date()
95+
.toISOString()
96+
.replace(/[:.]/g, "-")
97+
.split("T")[0];
98+
return `claude/${timestamp}-${Date.now()}`;
99+
})();
100+
console.log(chalk.blue(`Will create branch in container: ${branchName}`));
101+
}
52102

53103
// Discover credentials (optional - don't fail if not found)
54104
const credentials = await this.credentialManager.discover();

src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ export interface SandboxConfig {
2323
bashTimeout?: number;
2424
includeUntracked?: boolean;
2525
targetBranch?: string;
26+
remoteBranch?: string;
27+
prNumber?: string;
2628
}
2729

2830
export interface Credentials {

src/web-server.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,24 @@ export class WebUIServer {
9999
currentBranch = branchResult.stdout.trim();
100100
}
101101

102+
// Get repository remote URL for branch links
103+
let repoUrl = "";
104+
try {
105+
const remoteResult = await execAsync("git remote get-url origin", {
106+
cwd: this.originalRepo || process.cwd(),
107+
});
108+
const remoteUrl = remoteResult.stdout.trim();
109+
110+
// Convert SSH URLs to HTTPS for web links
111+
if (remoteUrl.startsWith("git@github.com:")) {
112+
repoUrl = remoteUrl.replace("git@github.com:", "https://github.com/").replace(".git", "");
113+
} else if (remoteUrl.startsWith("https://")) {
114+
repoUrl = remoteUrl.replace(".git", "");
115+
}
116+
} catch (error) {
117+
console.warn("Could not get repository URL:", error);
118+
}
119+
102120
// Get PR info using GitHub CLI (always use original repo)
103121
let prs = [];
104122
try {
@@ -114,8 +132,12 @@ export class WebUIServer {
114132
console.warn("Could not fetch PR info:", error);
115133
}
116134

135+
const branchUrl = repoUrl ? `${repoUrl}/tree/${currentBranch}` : "";
136+
117137
res.json({
118138
currentBranch,
139+
branchUrl,
140+
repoUrl,
119141
prs,
120142
});
121143
} catch (error) {

0 commit comments

Comments
 (0)