git-stint 0.2.4 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/adapters/claude-code/hooks/git-stint-hook-pre-tool +2 -1
- package/dist/git.d.ts +10 -0
- package/dist/git.js +33 -0
- package/dist/install-hooks.js +13 -1
- package/dist/session.js +47 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -96,7 +96,7 @@ git stint end
|
|
|
96
96
|
| `git stint squash -m "msg"` | Collapse all commits into one |
|
|
97
97
|
| `git stint merge` | Merge session into current branch (no PR) |
|
|
98
98
|
| `git stint pr [--title "..."]` | Push branch and create GitHub PR |
|
|
99
|
-
| `git stint end` | Finalize session, clean up everything |
|
|
99
|
+
| `git stint end` | Finalize session, clean up everything (deletes remote branch if merged) |
|
|
100
100
|
| `git stint abort` | Discard session — delete all changes |
|
|
101
101
|
| `git stint undo` | Revert last commit, changes become pending |
|
|
102
102
|
| `git stint conflicts` | Check file overlap with other sessions |
|
|
@@ -346,6 +346,7 @@ Combined testing creates a temporary octopus merge of the specified sessions, ru
|
|
|
346
346
|
- **`execFileSync` everywhere** — array arguments prevent shell injection. No `execSync` with string interpolation.
|
|
347
347
|
- **Atomic manifest writes** — write to `.tmp`, then `rename()`. Crash-safe.
|
|
348
348
|
- **Symlinks for shared data** — gitignored dirs (caches, data) symlink into worktrees instead of being copied or lost.
|
|
349
|
+
- **Safe remote branch cleanup** — on `end`/`merge`, deletes the remote branch only when all changes are verified merged **on the remote**. Checks against remote tracking refs (`origin/main`), not local branches — so a local-only merge won't delete the remote branch until you push. Uses a two-tier check (commit ancestry + content diff) that handles regular merges, squash merges, and rebase merges. Unmerged branches are always preserved with a warning.
|
|
349
350
|
- **Zero runtime dependencies** — only Node.js built-ins. Dev deps are TypeScript and @types/node.
|
|
350
351
|
|
|
351
352
|
## git-stint vs GitButler
|
|
@@ -152,7 +152,8 @@ if [ "$MAIN_BRANCH_POLICY" = "prompt" ]; then
|
|
|
152
152
|
fi
|
|
153
153
|
echo "BLOCKED: Writing to main branch." >&2
|
|
154
154
|
echo "To allow, run: git stint allow-main --client-id $CLIENT_ID" >&2
|
|
155
|
-
echo "To create a session instead, run: git stint start <name>" >&2
|
|
155
|
+
echo "To create a session instead, run: git stint start <descriptive-name>" >&2
|
|
156
|
+
echo " (pick a short name that describes your task, e.g. fix-auth-refresh, add-user-search)" >&2
|
|
156
157
|
exit 2
|
|
157
158
|
fi
|
|
158
159
|
|
package/dist/git.d.ts
CHANGED
|
@@ -20,6 +20,16 @@ export declare function deleteBranch(name: string): void;
|
|
|
20
20
|
*/
|
|
21
21
|
export declare function remoteBranchExists(name: string): boolean;
|
|
22
22
|
export declare function deleteRemoteBranch(name: string): void;
|
|
23
|
+
/**
|
|
24
|
+
* Check if all changes from `branch` are present in `into`.
|
|
25
|
+
* Handles regular merges, squash merges, and rebase merges.
|
|
26
|
+
*
|
|
27
|
+
* 1. Fast path: branch tip is an ancestor of `into` (regular merge / ff).
|
|
28
|
+
* 2. Content check: every file the branch changed has identical content in
|
|
29
|
+
* `into`. This catches squash and rebase merges where SHAs differ but
|
|
30
|
+
* the content is the same.
|
|
31
|
+
*/
|
|
32
|
+
export declare function isBranchMergedInto(branch: string, into: string): boolean;
|
|
23
33
|
export declare function addWorktree(path: string, branch: string): void;
|
|
24
34
|
/** Create a worktree in detached HEAD mode at a given ref. */
|
|
25
35
|
export declare function addWorktreeDetached(path: string, ref: string): void;
|
package/dist/git.js
CHANGED
|
@@ -78,6 +78,39 @@ export function remoteBranchExists(name) {
|
|
|
78
78
|
export function deleteRemoteBranch(name) {
|
|
79
79
|
git("push", "origin", "--delete", name);
|
|
80
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Check if all changes from `branch` are present in `into`.
|
|
83
|
+
* Handles regular merges, squash merges, and rebase merges.
|
|
84
|
+
*
|
|
85
|
+
* 1. Fast path: branch tip is an ancestor of `into` (regular merge / ff).
|
|
86
|
+
* 2. Content check: every file the branch changed has identical content in
|
|
87
|
+
* `into`. This catches squash and rebase merges where SHAs differ but
|
|
88
|
+
* the content is the same.
|
|
89
|
+
*/
|
|
90
|
+
export function isBranchMergedInto(branch, into) {
|
|
91
|
+
// Fast path — works for regular merges and fast-forwards
|
|
92
|
+
try {
|
|
93
|
+
git("merge-base", "--is-ancestor", branch, into);
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// Not an ancestor — may still be squash/rebase merged
|
|
98
|
+
}
|
|
99
|
+
// Content check — compare files the branch changed against `into`
|
|
100
|
+
try {
|
|
101
|
+
const mergeBase = git("merge-base", into, branch);
|
|
102
|
+
const changedOutput = git("diff", "--name-only", mergeBase, branch);
|
|
103
|
+
if (!changedOutput)
|
|
104
|
+
return true; // branch has no file changes
|
|
105
|
+
const files = changedOutput.split("\n");
|
|
106
|
+
const contentDiff = git("diff", into, branch, "--", ...files);
|
|
107
|
+
return contentDiff.length === 0;
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
// If anything fails (e.g. branch already deleted), assume not merged
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
81
114
|
export function addWorktree(path, branch) {
|
|
82
115
|
git("worktree", "add", path, branch);
|
|
83
116
|
}
|
package/dist/install-hooks.js
CHANGED
|
@@ -93,9 +93,17 @@ const RULES_CONTENT = `# Git Stint Workflow
|
|
|
93
93
|
All file edits are intercepted by git-stint hooks and redirected to isolated
|
|
94
94
|
worktrees. One stint session = one branch = one PR.
|
|
95
95
|
|
|
96
|
+
## Session Naming
|
|
97
|
+
|
|
98
|
+
When creating a session, pick a short descriptive name that captures the task:
|
|
99
|
+
- Good: \`fix-auth-refresh\`, \`add-user-search\`, \`refactor-db-queries\`
|
|
100
|
+
- Bad: \`session-1\`, \`changes\`, \`test\`, \`update\`
|
|
101
|
+
|
|
102
|
+
The name becomes the branch (\`stint/<name>\`) and the PR title context.
|
|
103
|
+
|
|
96
104
|
## Session Lifecycle
|
|
97
105
|
|
|
98
|
-
-
|
|
106
|
+
- If the hook blocks a write, create a session: \`git stint start <descriptive-name>\`
|
|
99
107
|
- All edits redirect to \`.stint/<session>/\` worktree.
|
|
100
108
|
- \`git stint commit -m "msg"\` to commit logical units of work.
|
|
101
109
|
- \`git stint pr\` to push and create PR.
|
|
@@ -103,6 +111,10 @@ worktrees. One stint session = one branch = one PR.
|
|
|
103
111
|
|
|
104
112
|
## Rules
|
|
105
113
|
|
|
114
|
+
- **NEVER end or delete a stint session you didn't create.** Other sessions
|
|
115
|
+
belong to other conversations or agents. Only operate on your own session
|
|
116
|
+
(the one auto-created by the hook for your edits). Use \`git stint list\` to
|
|
117
|
+
see all sessions — leave others alone.
|
|
106
118
|
- Do NOT call \`git stint end\` until all changes are committed (code, tests,
|
|
107
119
|
config updates, follow-up tasks). Premature \`end\` kills the session; the
|
|
108
120
|
next edit auto-creates a NEW session, fragmenting work across multiple PRs.
|
package/dist/session.js
CHANGED
|
@@ -696,15 +696,58 @@ function cleanup(manifest, force = false) {
|
|
|
696
696
|
}
|
|
697
697
|
}
|
|
698
698
|
}
|
|
699
|
+
// Check if remote branch should be cleaned up (before deleting local branch,
|
|
700
|
+
// since we need it for the merge check).
|
|
701
|
+
//
|
|
702
|
+
// IMPORTANT: We check against REMOTE tracking refs (origin/main), not local
|
|
703
|
+
// branches. A local `git stint merge` merges into local main, but the user
|
|
704
|
+
// may not have pushed yet. Deleting the remote session branch before the
|
|
705
|
+
// remote main has the changes would destroy the only remote copy of the work.
|
|
706
|
+
// By checking origin/main, we only delete when the remote already has the
|
|
707
|
+
// changes — matching how GitHub/GitLab auto-delete works.
|
|
708
|
+
let shouldDeleteRemote = false;
|
|
709
|
+
if (git.remoteBranchExists(manifest.branch)) {
|
|
710
|
+
// Build list of remote tracking refs to check against.
|
|
711
|
+
const targets = new Set();
|
|
712
|
+
const defaultBranch = git.getDefaultBranch();
|
|
713
|
+
targets.add(`origin/${defaultBranch}`);
|
|
714
|
+
try {
|
|
715
|
+
const current = git.currentBranch(topLevel);
|
|
716
|
+
if (current !== manifest.branch && git.remoteBranchExists(current)) {
|
|
717
|
+
targets.add(`origin/${current}`);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
catch { /* detached HEAD — skip */ }
|
|
721
|
+
for (const target of targets) {
|
|
722
|
+
if (git.isBranchMergedInto(manifest.branch, target)) {
|
|
723
|
+
shouldDeleteRemote = true;
|
|
724
|
+
break;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
if (!shouldDeleteRemote) {
|
|
728
|
+
console.log(`Remote branch 'origin/${manifest.branch}' was NOT deleted (has unmerged changes).\n` +
|
|
729
|
+
` To delete manually: git push origin --delete ${manifest.branch}`);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
699
732
|
// Delete local branch
|
|
700
733
|
try {
|
|
701
734
|
git.deleteBranch(manifest.branch);
|
|
702
735
|
}
|
|
703
736
|
catch { /* branch may already be deleted */ }
|
|
704
|
-
// Delete remote
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
737
|
+
// Delete remote branch if all changes are merged
|
|
738
|
+
if (shouldDeleteRemote) {
|
|
739
|
+
try {
|
|
740
|
+
git.deleteRemoteBranch(manifest.branch);
|
|
741
|
+
console.log(`Deleted remote branch 'origin/${manifest.branch}'.`);
|
|
742
|
+
}
|
|
743
|
+
catch {
|
|
744
|
+
// Branch may already be deleted on the remote (e.g., GitHub auto-delete
|
|
745
|
+
// after PR merge) or the network may be down. Either way, not critical.
|
|
746
|
+
console.log(`Could not delete remote branch 'origin/${manifest.branch}'.\n` +
|
|
747
|
+
` It may have already been deleted (e.g., by GitHub after PR merge).\n` +
|
|
748
|
+
` To delete manually: git push origin --delete ${manifest.branch}`);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
708
751
|
// Delete manifest last — if anything above fails, manifest persists for prune
|
|
709
752
|
deleteManifest(manifest.name);
|
|
710
753
|
}
|