Skip to content

Repository files navigation

iOS 15+macOS 12+Swift 5.9+SPMMIT

Gitty

A Swift package that wraps libgit2 with an idiomatic, async-first API designed for apps that work with real-world, authenticated repositories.

Website:gittykit.dev · Website repoPlatforms: iOS 15+ · macOS 12+ Swift: 5.9+

Why Gitty?

Existing Swift libgit2 wrappers either lack SPM support, are unmaintained, or are missing operations that real-world apps need: authenticated push/pull, typed diffs, merge with conflict detection, stash, rebase, blame, and worktrees. Gitty provides all of these with a modern async/await API and zero system dependencies.


Installation

dependencies:[.package(url:"https://github.com/RonenMars/Gitty", from:"0.1.0"),],targets:[.target(name:"MyApp", dependencies:["Gitty"]),]

Dependency

Gitty depends on RonenMars/libgit2, a fork of the official libgit2/libgit2 C library packaged for Swift Package Manager. No other dependencies.


Real-world walkthrough

1 · Clone a private repository

import Gitty
letrepo=tryawaitRepository.clone(
from:URL(string:"https://github.com/alice/private-app")!,
to: localURL,
credentials:.token("ghp_yourPersonalAccessToken"),
progress:{print("\(Int($0.fractionCompleted *100))%")})

2 · Stage and commit with a custom author

letauthor=Signature(name:"Alice", email:"alice@example.com")try repo.stage(paths:["Sources/Login.swift","README.md"])
// or stage everything: try repo.stageAll()
letcommit=try repo.commit(
message:"feat: add login screen",
author: author
)

3 · Push to origin

tryawait repo.remotes.push(to:"origin", credentials:.token("ghp_..."))

4 · Stream commit history

fortryawaitcommitin repo.log(limit:50){print("\(commit.id.abbreviated)\(commit.author)\(commit.subject)")}

5 · Diff

letdiffs=try repo.diff() // unstaged changes
letdiffs=try repo.diff(from:"HEAD") // working tree vs HEAD
letdiffs=try repo.diff(from: commitA, to: commitB)fordiffin diffs {print("\(diff.status)\(diff.newPath ??"") (+\(diff.linesAdded) -\(diff.linesDeleted))")forhunkin diff.hunks {print(hunk.header)}}

6 · Merge

letfeature=try repo.branches.list().first{ $0.name =="feature/login"}!
switchtry repo.merge(branch: feature){case.upToDate:print("Already up-to-date.")case.fastForward(let c):print("Fast-forwarded to \(c.id.abbreviated).")case.merged:try repo.commit(message:"Merge 'feature/login'", author: author)case.conflict(let fs):print("Conflicts: \(fs.map(\.path))")}

7 · Rebase

letmain=try repo.branches.list().first{ $0.name =="main"}!
switchtry repo.rebase(onto: main, author: author){case.success(let commits):print("Rebased \(commits.count) commits.")case.conflict(let files):print("Resolve: \(files.map(\.path))")}

8 · Cherry-pick

switchtry repo.cherryPick(someCommit){case.success:try repo.commit(message: someCommit.message, author: author)case.conflict(let files):print("Conflicts: \(files.map(\.path))")}

9 · Blame

lethunks=try repo.blame(file:"Sources/App/Login.swift")forhunkin hunks {print("\(hunk.lineRange)\(hunk.author)\(hunk.commitID.abbreviated)")}

10 · Stash

try repo.stash.push(message:"WIP", author: author, includeUntracked:true)letentries=try repo.stash.list()try repo.stash.pop() // restore + remove
try repo.stash.apply() // restore, keep on stack
try repo.stash.drop() // discard

11 · Branches

letbranches=try repo.branches.list()letbranch=try repo.branches.create(named:"feature/x", at: headCommit)try repo.branches.checkout(branch)try repo.branches.rename(from:"old", to:"new")try repo.branches.delete(named:"stale")

12 · Remotes

letremotes=try repo.remotes.list()try repo.remotes.add(name:"upstream", url:"https://github.com/upstream/repo")tryawait repo.remotes.fetch(named:"origin", credentials:.token("ghp_..."))try repo.remotes.remove(named:"old-remote")

13 · Tags

lettags=try repo.tags.list()try repo.tags.create(named:"v1.0.0", at: commit)try repo.tags.create(named:"v1.0.0", at: commit, message:"Release 1.0.0", tagger: author)try repo.tags.delete(named:"v0.9.0-beta")

14 · Worktrees

letworktrees=try repo.worktreeList()try repo.addWorktree(name:"hotfix", path:URL(fileURLWithPath:"/tmp/hotfix"), branch:"hotfix/v1")try repo.removeWorktree(named:"hotfix")

API overview

Repository
├── static clone(from:to:credentials:progress:) async throws → Repository
├── static open(at:) / initialize(at:) / exists(at:)
│
├── status(includeUntracked:) throws → [StatusEntry]
├── stage(paths:) / stageAll() / unstage(paths:) throws
├── commit(message:author:) throws → Commit
│
├── log(limit:) / log(from:limit:) → CommitLog (AsyncSequence)
│
├── diff() / diff(from:) / diff(from:to:) throws → [FileDiff]
│
├── merge(branch:) throws → MergeResult
├── rebase(onto:author:) / abortRebase() throws → RebaseResult
├── cherryPick(_:) throws → CherryPickResult
├── blame(file:) throws → [BlameHunk]
│
├── worktreeList() / addWorktree / removeWorktree / lock / unlock
│
├── branches — list / create / delete / rename / checkout
├── remotes — list / add / remove / rename / fetch / push
├── tags — list / create / delete
├── stash — push / pop / apply / drop / list
├── config — get / set / list / delete (planned)
└── submodules — list / initialize / update (planned)

Credentials

CaseUse for
.token("ghp_...")GitHub / GitLab / Bitbucket PATs over HTTPS
.usernamePassword(username:password:)Basic auth or PAT-as-password
.sshAgentSSH keys via a running agent
.defaultPublic repos / system credential helpers

Error handling

All operations throw GittyError with a code (raw libgit2 error code) and a message:

do{tryawait repo.remotes.push(to:"origin", credentials:.token("bad"))}catchlet err as GittyError{print("Push failed (\(err.code)): \(err.message)")}

License

MIT

About

Idiomatic Swift bindings for libgit2 — built for apps that don't just read Git, they write it.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages