surshar / claude-code
Claude Code is an agentic coding tool that lives in your terminal, understands your codebase, and helps you code faster by executing routine tasks, explaining complex code, and handling git workflows - all through natural language commands.
Built by Anthropic · github.com/anthropics
| @@ -14,6 +14,7 @@ | ||
| 14 | 14 | export * from './merge-base-candidates-of' |
| 15 | 15 | export * from './missing-base-of' |
| 16 | 16 | export * from './overlay-unstaged.js' |
| 17 | +export * from './rebase-marks' | |
| 17 | 18 | export * from './repository-lines' |
| 18 | 19 | export * from './repository-of' |
| 19 | 20 | export * from './stamp-if-file' |
| @@ -1,22 +1,38 @@ | ||
| 1 | 1 | import type Types from '../types' |
| 2 | +import { REBASE_MARKS } from './rebase-marks' | |
| 2 | 3 | import { TRANSIENT_STATE_FILES } from './transient-state-files' |
| 3 | 4 | |
| 4 | 5 | /** |
| 5 | 6 | * Whether the repository is mid-merge, mid-rebase, mid-cherry-pick or |
| 6 | - * mid-revert, from one listing of the git directory. | |
| 7 | + * mid-revert, from a listing of the git directory. | |
| 7 | 8 | * |
| 8 | - * A state file counts only as a real file; a symbolic link by that name is | |
| 9 | - * never touched. | |
| 9 | + * A state file counts only as a real file and a rebase's folder only as a | |
| 10 | + * real directory, listed once more to tell a rebase's from `git am`'s; a | |
| 11 | + * symbolic link by any of those names is never touched. | |
| 10 | 12 | * |
| 11 | 13 | * @param entryKindsOf the host's directory listing |
| 12 | 14 | * @param gitDir the repository's own git directory, absolute |
| 13 | - * @returns whether any of TRANSIENT_STATE_FILES is present as a file | |
| 15 | + * @returns whether a state file, or a rebase's file beside its folder, is there | |
| 14 | 16 | */ |
| 15 | 17 | export async function isTransient( |
| 16 | 18 | entryKindsOf: Types.GitDeps['entryKindsOf'], |
| 17 | 19 | gitDir: string, |
| 18 | 20 | ): Promise<boolean> { |
| 19 | 21 | const entries = await entryKindsOf(gitDir) |
| 22 | + const isStopped = entries?.get(REBASE_MARKS.head) === 'file' | |
| 23 | + const isApplying = isStopped && entries?.get(REBASE_MARKS.apply) === 'dir' | |
| 20 | 24 | |
| 21 | - return TRANSIENT_STATE_FILES.some(file => entries?.get(file) === 'file') | |
| 25 | + const applied = isApplying | |
| 26 | + ? await entryKindsOf(`${gitDir}/${REBASE_MARKS.apply}`) | |
| 27 | + : null | |
| 28 | + | |
| 29 | + const isRebasing = | |
| 30 | + isStopped && | |
| 31 | + (entries?.get(REBASE_MARKS.merge) === 'dir' || | |
| 32 | + applied?.get(REBASE_MARKS.rebasing) === 'file') | |
| 33 | + | |
| 34 | + return ( | |
| 35 | + isRebasing || | |
| 36 | + TRANSIENT_STATE_FILES.some(file => entries?.get(file) === 'file') | |
| 37 | + ) | |
| 22 | 38 | } |
| @@ -1,0 +1,3 @@ | ||
| 1 | +export * from './rebase-marks.js' | |
| 2 | + | |
| 3 | +export * as default from '.' |
| @@ -1,0 +1,14 @@ | ||
| 1 | +/** | |
| 2 | + * What in the git directory says a rebase stopped mid-way: `head`, the file | |
| 3 | + * naming the commit it stopped at, beside a folder the rebase still keeps. | |
| 4 | + * | |
| 5 | + * `head` alone is no sign: git leaves it behind a rebase that finished. | |
| 6 | + * The folder is `merge`, or `apply` while it holds the file `rebasing` | |
| 7 | + * (without it the folder is a stopped `git am`'s). | |
| 8 | + */ | |
| 9 | +export const REBASE_MARKS = { | |
| 10 | + head: 'REBASE_HEAD', | |
| 11 | + merge: 'rebase-merge', | |
| 12 | + apply: 'rebase-apply', | |
| 13 | + rebasing: 'rebasing', | |
| 14 | +} as const |
| @@ -1,10 +1,9 @@ | ||
| 1 | 1 | /** |
| 2 | - * The git-directory files that mean a merge, rebase, cherry-pick or revert | |
| 3 | - * is under way: the working tree then holds incoming changes, unread. | |
| 2 | + * The git-directory files that mean a merge, cherry-pick or revert is under | |
| 3 | + * way: the working tree then holds incoming changes, unread. | |
| 4 | 4 | */ |
| 5 | 5 | export const TRANSIENT_STATE_FILES = [ |
| 6 | 6 | 'MERGE_HEAD', |
| 7 | - 'REBASE_HEAD', | |
| 8 | 7 | 'CHERRY_PICK_HEAD', |
| 9 | 8 | 'REVERT_HEAD', |
| 10 | 9 | ] as const |
| @@ -1,0 +1,9 @@ | ||
| 1 | +import type Git from '../../../hooks/git' | |
| 2 | + | |
| 3 | +/** | |
| 4 | + * What the folder rebase-apply holds while `git am` owns it: the file | |
| 5 | + * `applying`, and no `rebasing`. | |
| 6 | + */ | |
| 7 | +export const APPLY_MAILING: readonly (readonly [string, Git.EntryKind])[] = [ | |
| 8 | + ['applying', 'file'], | |
| 9 | +] |
| @@ -1,0 +1,9 @@ | ||
| 1 | +import type Git from '../../../hooks/git' | |
| 2 | + | |
| 3 | +/** | |
| 4 | + * What the folder rebase-apply holds while a rebase owns it: the file | |
| 5 | + * `rebasing`. | |
| 6 | + */ | |
| 7 | +export const APPLY_REBASING: readonly (readonly [string, Git.EntryKind])[] = [ | |
| 8 | + ['rebasing', 'file'], | |
| 9 | +] |
| @@ -1,12 +1,20 @@ | ||
| 1 | +export * from './apply-mailing.js' | |
| 2 | +export * from './apply-rebasing.js' | |
| 1 | 3 | export * from './before-session-ms.js' |
| 2 | 4 | export * from './deep-directories.js' |
| 3 | 5 | export * from './during-session-ms.js' |
| 4 | 6 | export * from './fetch-repository.js' |
| 5 | 7 | export * from './files-listing.js' |
| 6 | 8 | export * from './git-session-start-ms.js' |
| 9 | +export * from './mailing.js' | |
| 7 | 10 | export * from './merge-head-linked.js' |
| 8 | 11 | export * from './merging.js' |
| 9 | 12 | export * from './one-staged.js' |
| 13 | +export * from './rebase-applying.js' | |
| 14 | +export * from './rebase-head-left.js' | |
| 15 | +export * from './rebase-linked.js' | |
| 16 | +export * from './rebase-merging.js' | |
| 17 | +export * from './rebase-paused.js' | |
| 10 | 18 | export * from './vs-head.js' |
| 11 | 19 | |
| 12 | 20 | export * as default from '.' |
| @@ -1,0 +1,9 @@ | ||
| 1 | +import type Git from '../../../hooks/git' | |
| 2 | + | |
| 3 | +/** | |
| 4 | + * A git dir listing while `git am` stopped on a conflict: the folder it | |
| 5 | + * shares with a rebase by name, and no REBASE_HEAD. | |
| 6 | + */ | |
| 7 | +export const MAILING: readonly (readonly [string, Git.EntryKind])[] = [ | |
| 8 | + ['rebase-apply', 'dir'], | |
| 9 | +] |
| @@ -1,0 +1,10 @@ | ||
| 1 | +import type Git from '../../../hooks/git' | |
| 2 | + | |
| 3 | +/** | |
| 4 | + * A git dir listing while a rebase that applies patches stopped on a | |
| 5 | + * conflict; its folder holds APPLY_REBASING. | |
| 6 | + */ | |
| 7 | +export const REBASE_APPLYING: readonly (readonly [string, Git.EntryKind])[] = [ | |
| 8 | + ['REBASE_HEAD', 'file'], | |
| 9 | + ['rebase-apply', 'dir'], | |
| 10 | +] |
| @@ -1,0 +1,9 @@ | ||
| 1 | +import type Git from '../../../hooks/git' | |
| 2 | + | |
| 3 | +/** | |
| 4 | + * A git dir listing after a rebase that finished: git left REBASE_HEAD behind | |
| 5 | + * and removed the rebase's folder. | |
| 6 | + */ | |
| 7 | +export const REBASE_HEAD_LEFT: readonly (readonly [string, Git.EntryKind])[] = [ | |
| 8 | + ['REBASE_HEAD', 'file'], | |
| 9 | +] |
| @@ -1,0 +1,10 @@ | ||
| 1 | +import type Git from '../../../hooks/git' | |
| 2 | + | |
| 3 | +/** | |
| 4 | + * A git dir listing whose rebase-merge is a symbolic link beside a | |
| 5 | + * REBASE_HEAD, so no rebase is under way as far as the fetch is concerned. | |
| 6 | + */ | |
| 7 | +export const REBASE_LINKED: readonly (readonly [string, Git.EntryKind])[] = [ | |
| 8 | + ['REBASE_HEAD', 'file'], | |
| 9 | + ['rebase-merge', 'other'], | |
| 10 | +] |
| @@ -1,0 +1,10 @@ | ||
| 1 | +import type Git from '../../../hooks/git' | |
| 2 | + | |
| 3 | +/** | |
| 4 | + * A git dir listing while a rebase stopped on a conflict: the commit it | |
| 5 | + * stopped at beside the folder the rebase keeps. | |
| 6 | + */ | |
| 7 | +export const REBASE_MERGING: readonly (readonly [string, Git.EntryKind])[] = [ | |
| 8 | + ['REBASE_HEAD', 'file'], | |
| 9 | + ['rebase-merge', 'dir'], | |
| 10 | +] |
| @@ -1,0 +1,9 @@ | ||
| 1 | +import type Git from '../../../hooks/git' | |
| 2 | + | |
| 3 | +/** | |
| 4 | + * A git dir listing while a rebase waits at a `break`: its folder, and no | |
| 5 | + * REBASE_HEAD, which git writes only where a commit stopped it. | |
| 6 | + */ | |
| 7 | +export const REBASE_PAUSED: readonly (readonly [string, Git.EntryKind])[] = [ | |
| 8 | + ['rebase-merge', 'dir'], | |
| 9 | +] |
| @@ -77,6 +77,22 @@ | ||
| 77 | 77 | 'ls-files': Fixtures.GIT_TIMED_OUT, |
| 78 | 78 | }) |
| 79 | 79 | |
| 80 | + const fetchedIn = (gitDir: Listing, applyDir: Listing = []) => | |
| 81 | + Git.fetchDiff( | |
| 82 | + depsOf(trackedNoUntrackedOf(), { | |
| 83 | + entryKindsOf: dir => | |
| 84 | + Promise.resolve( | |
| 85 | + listingOf( | |
| 86 | + new Map([ | |
| 87 | + ['/repo/.git', gitDir], | |
| 88 | + ['/repo/.git/rebase-apply', applyDir], | |
| 89 | + ]).get(dir) ?? Fixtures.FILES_LISTING, | |
| 90 | + ), | |
| 91 | + ), | |
| 92 | + }), | |
| 93 | + 'session', | |
| 94 | + ) | |
| 95 | + | |
| 80 | 96 | test('session mode: rows tagged by mtime, untracked merged', async () => { |
| 81 | 97 | const outcome = await Git.fetchDiff( |
| 82 | 98 | depsOf( |
| @@ -374,6 +390,48 @@ | ||
| 374 | 390 | expect(outcome.kind === 'data' && outcome.data.files).toHaveLength(1) |
| 375 | 391 | }) |
| 376 | 392 | |
| 393 | + test('a rebase stopped on a conflict: unavailable', async () => { | |
| 394 | + expect(await fetchedIn(Fixtures.REBASE_MERGING)).toEqual({ | |
| 395 | + kind: 'unavailable', | |
| 396 | + }) | |
| 397 | + }) | |
| 398 | + | |
| 399 | + test('a rebase applying patches, stopped: unavailable', async () => { | |
| 400 | + expect( | |
| 401 | + await fetchedIn(Fixtures.REBASE_APPLYING, Fixtures.APPLY_REBASING), | |
| 402 | + ).toEqual({ kind: 'unavailable' }) | |
| 403 | + }) | |
| 404 | + | |
| 405 | + test('a REBASE_HEAD a finished rebase left is no rebase', async () => { | |
| 406 | + expect(await fetchedIn(Fixtures.REBASE_HEAD_LEFT)).toMatchObject({ | |
| 407 | + kind: 'data', | |
| 408 | + }) | |
| 409 | + }) | |
| 410 | + | |
| 411 | + test('git am beside a REBASE_HEAD left behind is no rebase', async () => { | |
| 412 | + expect( | |
| 413 | + await fetchedIn(Fixtures.REBASE_APPLYING, Fixtures.APPLY_MAILING), | |
| 414 | + ).toMatchObject({ kind: 'data' }) | |
| 415 | + }) | |
| 416 | + | |
| 417 | + test('a rebase folder that is a symbolic link is no rebase', async () => { | |
| 418 | + expect(await fetchedIn(Fixtures.REBASE_LINKED)).toMatchObject({ | |
| 419 | + kind: 'data', | |
| 420 | + }) | |
| 421 | + }) | |
| 422 | + | |
| 423 | + test('git am stopped on a conflict reads as it did', async () => { | |
| 424 | + expect( | |
| 425 | + await fetchedIn(Fixtures.MAILING, Fixtures.APPLY_MAILING), | |
| 426 | + ).toMatchObject({ kind: 'data' }) | |
| 427 | + }) | |
| 428 | + | |
| 429 | + test('a rebase waiting at a break reads as it did', async () => { | |
| 430 | + expect(await fetchedIn(Fixtures.REBASE_PAUSED)).toMatchObject({ | |
| 431 | + kind: 'data', | |
| 432 | + }) | |
| 433 | + }) | |
| 434 | + | |
| 377 | 435 | test('a path under a symlinked directory: unlisted, undated', async () => { |
| 378 | 436 | const listed: string[] = [] |
| 379 | 437 | const probed: string[] = [] |