Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
874f7bc
feat: continue and skip rebase
craigharman Jun 19, 2026
f55ca09
fix: git tests for rebasing
craigharman Jun 19, 2026
7e07df3
feat: liquid glass interface
craigharman Jun 19, 2026
8f7c2c9
feat: add copy to clipboard button to error banner
craigharman Jun 19, 2026
3852ef3
Merge branch 'main' into develop
craigharman Jun 19, 2026
eac879e
feat:automatic software updates using Sparkle
craigharman Jun 19, 2026
9074f29
Gate the step on secrets.SPARKLE_PRIVATE_KEY
craigharman Jun 19, 2026
13f17fe
Ensure we are committing on the latest main branch
craigharman Jun 19, 2026
7487310
Disable update option if sparkle unavailable
craigharman Jun 19, 2026
354016d
Fail loudly on build failure
craigharman Jun 19, 2026
cd5dde1
Potential fix for pull request finding
craigharman Jun 19, 2026
d49ba06
fix: prevent appcast.xml commit from retriggering Release workflow
Copilot Jun 19, 2026
0c9ce1c
Strip any new lines from secret
craigharman Jun 19, 2026
3293625
Warn if sparkle not set
craigharman Jun 19, 2026
a6a5c24
Add guard when version already exists
craigharman Jun 19, 2026
ff59aa8
Fix versioning issues with sparkle
craigharman Jun 19, 2026
ee5d2b7
Potential fix for pull request finding
craigharman Jun 19, 2026
730c686
Potential fix for pull request finding
craigharman Jun 19, 2026
7b23eeb
Potential fix for pull request finding
craigharman Jun 19, 2026
7e96235
Skip appcast.xml when no changes
craigharman Jun 19, 2026
b904ab8
Update agents and claude files
craigharman Jun 19, 2026
4f1a868
fix: sign_update matched the wrong binary
craigharman Jun 19, 2026
895e093
Merge develop changes
craigharman Jun 19, 2026
a9338d0
Merge branch 'main' into develop
craigharman Jun 19, 2026
626e3f8
feat: allow push/pull indicators in sidebar to actually do the push/p…
craigharman Jun 19, 2026
7a1a0a6
fix: when pushing a remote branch include the remote name as well as …
craigharman Jun 19, 2026
70df5fd
Do not fallback to origin show error instead
craigharman Jun 19, 2026
ff5ed2c
fix: make the sparkle search less specific
craigharman Jun 19, 2026
0f530b7
Merge branch 'main' into develop
craigharman Jun 20, 2026
b3e181d
Fix release.yml
craigharman Jun 20, 2026
528f9e7
Merge branch 'main' into develop
craigharman Jun 20, 2026
ff6c4ac
fix: download Sparkle into project from CI
craigharman Jun 20, 2026
6e8b217
fix: access to sparkle key file
craigharman Jun 20, 2026
505d31a
Merge main into develop
craigharman Jun 20, 2026
93cafbd
Merge branch 'main' into develop
craigharman Jun 20, 2026
4028d96
fix: add debugging to deployment
craigharman Jun 20, 2026
fef2820
Merge branch 'main' into develop
craigharman Jun 20, 2026
25103c1
fix: add automerge to release
craigharman Jun 20, 2026
dfb183d
test:Add checks for pull/push numbers
craigharman Jun 21, 2026
6b7a74d
feat:automate deployment workflow
craigharman Jun 22, 2026
1707933
fix: merge conflicts not visible when files are staged
craigharman Jun 23, 2026
563c1d9
Complete merge
craigharman Jun 23, 2026
7a88a41
fix: use token for deployment
craigharman Jun 23, 2026
a0addf8
feat: add remote SSH git repository management
craigharman Jun 24, 2026
03f2837
fix: do not allow banner colour to bleed into sidebar
craigharman Jun 24, 2026
133bb9c
chore: updated clipping on main panel
craigharman Jun 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 37 additions & 6 deletions .github/workflows/auto-merge-release.yml
Original file line number Diff line number Diff line change
@@ -1,31 +1,62 @@
name: Auto-merge release-please PR

on:
pull_request:
# Triggers after the Release workflow completes on main.
# workflow_run always runs in the default-branch context, so it is never
# blocked by the "first-time contributor" approval gate.
workflow_run:
workflows: ["Release"]
types: [completed]
branches: [main]
types: [opened, synchronize, reopened]

permissions:
contents: write
pull-requests: write

jobs:
auto-merge:
# Only run on release-please PRs
if: github.actor == 'github-actions[bot]' && startsWith(github.head_ref, 'release-please--')
runs-on: macos-26
# Only proceed when the Release workflow succeeded
if: github.event.workflow_run.conclusion == 'success'
steps:
- name: Find open release-please PR
id: find-pr
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
PR_NUMBER=$(gh pr list \
--repo "${{ github.repository }}" \
--head "release-please--branches--main" \
--state open \
--json number \
--jq '.[0].number // empty')

if [[ -z "${PR_NUMBER}" ]]; then
echo "No open release-please PR found; nothing to do."
echo "found=false" >> "$GITHUB_OUTPUT"
else
echo "Found release-please PR #${PR_NUMBER}"
echo "found=true" >> "$GITHUB_OUTPUT"
echo "number=${PR_NUMBER}" >> "$GITHUB_OUTPUT"
fi

- uses: actions/checkout@v4
if: steps.find-pr.outputs.found == 'true'
with:
ref: release-please--branches--main

- name: Build
if: steps.find-pr.outputs.found == 'true'
run: swift build

- name: Run tests
if: steps.find-pr.outputs.found == 'true'
run: swift run GitKitChecks

- name: Approve and auto-merge
if: steps.find-pr.outputs.found == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh pr review "${{ github.event.pull_request.number }}" --approve
gh pr merge "${{ github.event.pull_request.number }}" --squash --auto
gh pr review "${{ steps.find-pr.outputs.number }}" --approve
gh pr merge "${{ steps.find-pr.outputs.number }}" --squash --auto
1 change: 1 addition & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ jobs:
id: release
with:
release-type: simple
token: ${{ secrets.RELEASE_TOKEN }}

build:
needs: release-please
Expand Down
17 changes: 14 additions & 3 deletions Sources/GitKit/CLIGitService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ private let recordSeparator = "\u{1e}" // between records
/// `GitService` backed by the user's installed `git` binary.
public struct CLIGitService: GitService {
public let root: URL
private let runner: GitRunner
private let runner: any GitRunnerProtocol

/// Creates a service rooted at `directory`. Resolves to the repository top-level so
/// that all paths are reported relative to a stable root.
/// Creates a service rooted at a local `directory`. Resolves to the repository
/// top-level so that all paths are reported relative to a stable root.
public init(directory: URL, executablePath: String? = nil) async throws {
let probe = GitRunner(workingDirectory: directory, executablePath: executablePath)
let result = try await probe.runRaw(["rev-parse", "--show-toplevel"])
Expand All @@ -24,6 +24,17 @@ public struct CLIGitService: GitService {
self.runner = GitRunner(workingDirectory: self.root, executablePath: executablePath)
}

/// Creates a service for a remote repository accessed over SSH.
public init(sshRunner: SSHGitRunner) async throws {
let result = try await sshRunner.runRaw(["rev-parse", "--show-toplevel"])
guard result.succeeded else {
throw GitError.notARepository(sshRunner.remotePath)
}
let top = result.stdoutString.trimmingCharacters(in: .whitespacesAndNewlines)
self.root = URL(fileURLWithPath: top)
self.runner = sshRunner
}

/// Discovers the repository top-level for an arbitrary path, returning nil if none.
public static func repositoryRoot(for directory: URL, executablePath: String? = nil) async -> URL? {
let probe = GitRunner(workingDirectory: directory, executablePath: executablePath)
Expand Down
2 changes: 1 addition & 1 deletion Sources/GitKit/GitRunner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ private final class CaptureBox: @unchecked Sendable {
///
/// An actor enforces one mutating invocation at a time per repository, avoiding
/// `index.lock` contention while still allowing concurrency across repositories.
public actor GitRunner {
public actor GitRunner: GitRunnerProtocol {
/// Working directory for invocations. `nil` for repo-independent commands (e.g. clone, version).
public let workingDirectory: URL?
private let executableURL: URL
Expand Down
36 changes: 36 additions & 0 deletions Sources/GitKit/GitRunnerProtocol.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import Foundation

/// Abstraction over local and SSH-based git command execution.
///
/// Both `GitRunner` (local) and `SSHGitRunner` (remote) conform to this protocol,
/// allowing `CLIGitService` to execute git commands transparently against either.
public protocol GitRunnerProtocol: Actor {
/// Working directory for the repository. For local runners this is a filesystem URL;
/// for SSH runners it\u{2019}s a synthetic URL representing the remote path.
var workingDirectory: URL? { get }

/// Runs `git <arguments>` and returns the result without throwing on non-zero exit.
func runRaw(_ arguments: [String]) async throws -> GitProcessResult

/// Runs `git <arguments>`, throwing on non-zero exit. Returns stdout as `Data`.
@discardableResult
func run(_ arguments: [String]) async throws -> Data

/// Runs `git <arguments>`, throwing on non-zero exit. Returns stdout as a string.
func runString(_ arguments: [String]) async throws -> String

/// Runs `git <arguments>`, streaming stderr progress lines to `onProgress`.
/// Throws on non-zero exit.
@discardableResult
func runStreaming(
_ arguments: [String],
onProgress: (@Sendable (String) -> Void)?
) async throws -> GitProcessResult
}

public extension GitRunnerProtocol {
@discardableResult
func runStreaming(_ arguments: [String]) async throws -> GitProcessResult {
try await runStreaming(arguments, onProgress: nil)
}
}
10 changes: 8 additions & 2 deletions Sources/GitKit/Models/FileStatus.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,14 @@ public struct WorkingTreeStatus: Sendable {
public let isDetached: Bool
public let files: [FileStatus]

public var stagedFiles: [FileStatus] { files.filter(\.isStaged) }
public var unstagedFiles: [FileStatus] { files.filter { $0.hasUnstagedChanges || $0.isUntracked } }
/// Conflicted (unmerged) files. Surfaced separately so they don't masquerade as ordinary
/// staged/unstaged edits — an unmerged entry reports both an index and a worktree state.
public var conflictedFiles: [FileStatus] { files.filter(\.isConflicted) }
public var stagedFiles: [FileStatus] { files.filter { $0.isStaged && !$0.isConflicted } }
public var unstagedFiles: [FileStatus] {
files.filter { !$0.isConflicted && ($0.hasUnstagedChanges || $0.isUntracked) }
}
public var hasConflicts: Bool { files.contains(where: \.isConflicted) }
public var hasChanges: Bool { !files.isEmpty }

public init(
Expand Down
8 changes: 7 additions & 1 deletion Sources/GitKit/RepositoryWatcher.swift
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import Foundation

/// Common interface for local (FSEvents) and remote (SSH polling) repository watchers.
public protocol RepositoryWatching: AnyObject, Sendable {
func start()
func stop()
}

/// Watches a repository's working tree (and its `.git` directory) with FSEvents, invoking
/// `onChange` when anything changes on disk. Events are coalesced by FSEvents' latency
/// window; callers should debounce further before doing expensive work.
public final class RepositoryWatcher: @unchecked Sendable {
public final class RepositoryWatcher: RepositoryWatching, @unchecked Sendable {
private let path: String
private let onChange: @Sendable () -> Void
private let queue = DispatchQueue(label: "com.gitify.repository-watcher", qos: .utility)
Expand Down
Loading
Loading