普通视图

发现新文章,点击刷新页面。
今天 — 2026年1月24日iOS

__CFRunLoopServiceMachPort函数详解

作者 iOS在入门
2026年1月23日 18:45

借助AI辅助。

__CFRunLoopServiceMachPort 函数逐行注释

这是 RunLoop 在 macOS 上休眠和唤醒的核心函数,通过 mach_msg() 系统调用实现线程阻塞。


完整注释代码

static Boolean __CFRunLoopServiceMachPort(
    mach_port_name_t port,              // 要等待的端口(或端口集合)
    mach_msg_header_t **buffer,         // 消息缓冲区指针的地址
    size_t buffer_size,                 // 缓冲区大小
    mach_port_t *livePort,              // [输出] 被唤醒的端口
    mach_msg_timeout_t timeout,         // 超时时间(毫秒,TIMEOUT_INFINITY=无限)
    voucher_mach_msg_state_t *_Nonnull voucherState,  // voucher 状态(追踪用)
    voucher_t *voucherCopy,             // voucher 副本
    CFRunLoopRef rl,                    // RunLoop(用于追踪)
    CFRunLoopModeRef rlm                // Mode(用于追踪)
) {
    // ========================================
    // 函数返回值说明:
    // • true: 收到消息,livePort 指向唤醒的端口
    // • false: 超时或错误
    // ========================================
    
    Boolean originalBuffer = true;
    // 标记是否使用原始缓冲区
    // true: 使用调用者提供的栈上缓冲区
    // false: 使用动态分配的堆缓冲区(消息太大时)
    
    kern_return_t ret = KERN_SUCCESS;
    // Mach 内核调用的返回值
    // 初始化为成功状态
    
    for (;;) {
        // 无限循环,直到:
        // 1. 成功接收消息(return true)
        // 2. 超时(return false)
        // 3. 致命错误(break 后 HALT)
        
        /* In that sleep of death what nightmares may come ... */
        // 莎士比亚《哈姆雷特》引用:"在死亡的睡眠中会有什么噩梦来临..."
        // 暗示线程即将进入"休眠"状态
        
        mach_msg_header_t *msg = (mach_msg_header_t *)*buffer;
        // 获取消息指针
        // *buffer 是指向缓冲区的指针
        
        // ========================================
        // 步骤 1: 初始化消息头
        // ========================================
        
        msg->msgh_bits = 0;
        // 消息标志位,初始化为 0
        // mach_msg 会设置适当的接收标志
        
        msg->msgh_local_port = port;
        // 设置本地端口(接收端口)
        // 这是我们要等待的端口(或端口集合)
        
        msg->msgh_remote_port = MACH_PORT_NULL;
        // 远程端口(发送目标)设为空
        // 因为我们只接收,不发送
        
        msg->msgh_size = buffer_size;
        // 设置缓冲区大小
        // 告诉内核我们能接收多大的消息
        
        msg->msgh_id = 0;
        // 消息 ID,初始化为 0
        // 接收后会包含实际的消息 ID
        
        // ========================================
        // 步骤 2: 记录追踪事件(调试用)
        // ========================================
        
        if (TIMEOUT_INFINITY == timeout) {
            // 如果是无限等待
            CFRUNLOOP_SLEEP();
            // 探针宏:记录休眠事件(DTrace)
            cf_trace(KDEBUG_EVENT_CFRL_SLEEP, port, 0, 0, 0);
            // 内核追踪:记录 RunLoop 即将休眠
        } else {
            // 如果有超时时间(轮询模式)
            CFRUNLOOP_POLL();
            // 探针宏:记录轮询事件
            cf_trace(KDEBUG_EVENT_CFRL_POLL, port, 0, 0, 0);
            // 内核追踪:记录 RunLoop 轮询
        }
        
        cf_trace(KDEBUG_EVENT_CFRL_RUN | DBG_FUNC_END, rl, rlm, port, timeout);
        // 追踪:RunLoop 运行阶段结束
        
        cf_trace(KDEBUG_EVENT_CFRL_IS_WAITING | DBG_FUNC_START, rl, rlm, port, timeout);
        // 追踪:开始等待阶段
        
        // ========================================
        // 步骤 3: 调用 mach_msg 等待 ⭐⭐⭐
        // 【这是整个 RunLoop 最核心的一行代码!】
        // ========================================
        
        ret = mach_msg(
            msg,                    // 消息缓冲区
            // 选项组合:
            MACH_RCV_MSG |          // 接收消息模式
            MACH_RCV_VOUCHER |      // 接收 voucher(追踪信息)
            MACH_RCV_LARGE |        // 支持大消息(自动重新分配缓冲区)
            ((TIMEOUT_INFINITY != timeout) ? MACH_RCV_TIMEOUT : 0) | 
            // 如果有超时,添加 MACH_RCV_TIMEOUT 标志
            MACH_RCV_TRAILER_TYPE(MACH_MSG_TRAILER_FORMAT_0) |
            // 接收 trailer(消息尾部附加信息)
            MACH_RCV_TRAILER_ELEMENTS(MACH_RCV_TRAILER_AV),
            // trailer 包含 audit token 和 voucher
            0,                      // 发送大小(不发送,所以为 0)
            msg->msgh_size,         // 接收缓冲区大小
            port,                   // 接收端口(或端口集合)
            timeout,                // 超时时间(毫秒)
            MACH_PORT_NULL          // 通知端口(不使用)
        );
        // 【线程在这里阻塞】
        // 等待以下情况之一:
        // 1. port 收到消息 → 返回 MACH_MSG_SUCCESS
        // 2. 超时 → 返回 MACH_RCV_TIMED_OUT
        // 3. 消息太大 → 返回 MACH_RCV_TOO_LARGE
        // 4. 其他错误 → 返回错误码
        
        cf_trace(KDEBUG_EVENT_CFRL_IS_WAITING | DBG_FUNC_END, rl, rlm, port, timeout);
        // 追踪:等待阶段结束(被唤醒)
        
        cf_trace(KDEBUG_EVENT_CFRL_RUN | DBG_FUNC_START, rl, rlm, port, timeout);
        // 追踪:RunLoop 运行阶段开始
        
        // ========================================
        // 步骤 4: 处理 voucher(性能追踪)
        // ========================================
        
        // Take care of all voucher-related work right after mach_msg.
        // 在 mach_msg 之后立即处理所有 voucher 相关工作
        // If we don't release the previous voucher we're going to leak it.
        // 如果不释放之前的 voucher,会造成内存泄漏
        
        voucher_mach_msg_revert(*voucherState);
        // 恢复之前的 voucher 状态
        // 释放上次收到的 voucher(如果有)
        
        // Someone will be responsible for calling voucher_mach_msg_revert. This call makes the received voucher the current one.
        // 调用者负责调用 voucher_mach_msg_revert
        // 这个调用让接收到的 voucher 成为当前的
        
        *voucherState = voucher_mach_msg_adopt(msg);
        // 采用(adopt)消息中的 voucher
        // 返回新的 voucher 状态
        // voucher 用于追踪消息的来源和上下文
        
        if (voucherCopy) {
            // 如果调用者需要 voucher 副本
            *voucherCopy = NULL;
            // 重置为 NULL
            // 调用者可以在需要时拷贝
        }

        CFRUNLOOP_WAKEUP(ret);
        // 探针宏:记录唤醒事件,传入返回值
        
        cf_trace(KDEBUG_EVENT_CFRL_DID_WAKEUP, port, 0, 0, 0);
        // 内核追踪:记录 RunLoop 被唤醒
        
        // ========================================
        // 步骤 5: 处理返回结果
        // ========================================
        
        if (MACH_MSG_SUCCESS == ret) {
            // 情况 1: 成功接收到消息
            
            *livePort = msg ? msg->msgh_local_port : MACH_PORT_NULL;
            // 返回被唤醒的端口
            // 调用者通过这个值判断唤醒源:
            // • _wakeUpPort → 手动唤醒
            // • _timerPort → 定时器
            // • dispatchPort → GCD 主队列
            // • 其他 → Source1
            
            return true;
            // 返回成功,结束函数
        }
        
        if (MACH_RCV_TIMED_OUT == ret) {
            // 情况 2: 接收超时(正常情况)
            
            if (!originalBuffer) free(msg);
            // 如果使用了动态分配的缓冲区,释放它
            
            *buffer = NULL;
            // 将缓冲区指针设为 NULL
            
            *livePort = MACH_PORT_NULL;
            // 没有唤醒端口(超时)
            
            return false;
            // 返回失败(超时)
        }
        
        if (MACH_RCV_TOO_LARGE != ret) {
            // 情况 3: 其他错误(非 "消息太大")
            // 这些是致命错误,需要崩溃
            
            if (((MACH_RCV_HEADER_ERROR & ret) == MACH_RCV_HEADER_ERROR) || 
                (MACH_RCV_BODY_ERROR & ret) == MACH_RCV_BODY_ERROR) {
                // 如果是消息头错误或消息体错误
                
                kern_return_t specialBits = MACH_MSG_MASK & ret;
                // 提取特殊错误位
                
                if (MACH_MSG_IPC_SPACE == specialBits) {
                    // IPC 空间不足
                    CRSetCrashLogMessage("Out of IPC space");
                    // 设置崩溃日志消息
                    // 可能原因:Mach 端口泄漏
                    
                } else if (MACH_MSG_VM_SPACE == specialBits) {
                    // 虚拟内存空间不足
                    CRSetCrashLogMessage("Out of VM address space");
                    // 内存耗尽
                    
                } else if (MACH_MSG_IPC_KERNEL == specialBits) {
                    // 内核 IPC 资源短缺
                    CRSetCrashLogMessage("Kernel resource shortage handling IPC");
                    // 内核资源不足
                    
                } else if (MACH_MSG_VM_KERNEL == specialBits) {
                    // 内核 VM 资源短缺
                    CRSetCrashLogMessage("Kernel resource shortage handling out-of-line memory");
                    // 内核内存不足
                }
            } else {
                // 其他类型的错误
                CRSetCrashLogMessage(mach_error_string(ret));
                // 设置错误字符串为崩溃日志
            }
            break;
            // 跳出循环,准备崩溃
        }
        
        // ========================================
        // 步骤 6: 处理 MACH_RCV_TOO_LARGE(消息太大)
        // ========================================
        // 如果执行到这里,说明 ret == MACH_RCV_TOO_LARGE
        
        buffer_size = round_msg(msg->msgh_size + MAX_TRAILER_SIZE);
        // 计算需要的缓冲区大小
        // round_msg: 向上取整到合适的大小
        // msg->msgh_size: 实际消息大小(已在 msg 中设置)
        // MAX_TRAILER_SIZE: trailer 的最大大小
        
        if (originalBuffer) *buffer = NULL;
        // 如果之前使用的是原始缓冲区(栈上的)
        // 将指针设为 NULL,下面会分配新的
        
        originalBuffer = false;
        // 标记不再使用原始缓冲区
        
        *buffer = __CFSafelyReallocate(*buffer, buffer_size, NULL);
        // 重新分配更大的缓冲区
        // 如果 *buffer 是 NULL,相当于 malloc
        // 否则相当于 realloc
        // 下次循环会使用新缓冲区重新接收

        if (voucherCopy != NULL && *voucherCopy != NULL) {
            // 如果有 voucher 副本
            os_release(*voucherCopy);
            // 释放 voucher(引用计数 -1)
        }
    }
    // 继续循环,使用新缓冲区重新调用 mach_msg
    
    HALT;
    // 如果跳出循环(因为致命错误),停止程序
    // HALT 宏会触发断点或终止进程
    
    return false;
    // 这行代码实际不会执行(HALT 不会返回)
    // 但保留以满足编译器要求
}

函数执行流程图

┌─────────────────────────────────────────────────────────────┐
│  开始 __CFRunLoopServiceMachPort                            │
└─────────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────────┐
│  初始化消息头                                                 │
│  • msgh_local_port = port (等待的端口)                        │
│  • msgh_size = buffer_size                                  │
│  • 其他字段置 0                                               │
└─────────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────────┐
│  记录追踪事件                                                 │
│  • SLEEP (无限等待) 或 POLL (有超时)                           │
└─────────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────────┐
│  ⭐ 调用 mach_msg() - 线程在此阻塞 ⭐                          │
│                                                             │
│  等待事件:                                                  │
│  • Timer 端口有消息                                          │
│  • Source1 端口有消息                                        │
│  • dispatch 端口有消息                                       │
│  • _wakeUpPort 有消息                                        │
│  • 超时                                                      │
└─────────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────────┐
│  被唤醒,检查返回值                                            │
└─────────────────────────────────────────────────────────────┘
          │               │                │
          ▼               ▼                ▼
    ┌─────────┐    ┌──────────┐    ┌──────────────┐
    │ SUCCESS │    │ TIMED_OUT│    │  TOO_LARGE   │
    └─────────┘    └──────────┘    └──────────────┘
          │               │                │
          ▼               ▼                ▼
    ┌─────────┐    ┌──────────┐    ┌──────────────┐
    │处理voucher│   │ 清理缓冲  │    │ 扩大缓冲区    │
    │返回true  │   │返回false  │     │ 重新接收     │
    └─────────┘    └──────────┘    └──────────────┘
                                          │
                                          ▼
                                    ┌──────────┐
                                    │返回循环开始│
                                    └──────────┘

关键点说明

1. mach_msg 的两种模式

模式 timeout 值 行为
休眠模式 TIMEOUT_INFINITY 永久阻塞,直到收到消息
轮询模式 0 或小值 立即返回或短暂等待

2. 可能的返回值

返回值 说明 处理方式
MACH_MSG_SUCCESS 成功收到消息 返回 true
MACH_RCV_TIMED_OUT 超时 返回 false
MACH_RCV_TOO_LARGE 消息太大 扩大缓冲区重试
其他错误 致命错误 崩溃(HALT)

3. voucher 的作用

voucher 是 macOS 的性能追踪机制:
├── 追踪消息来源
├── 记录 QoS(服务质量)
├── 性能分析(Instruments)
└── 调试辅助

4. 缓冲区管理

初始: 使用栈缓冲区(3KB)
  ↓
mach_msg 返回 TOO_LARGE
  ↓
计算实际大小: msg->msgh_size + MAX_TRAILER_SIZE
  ↓
动态分配堆缓冲区
  ↓
重新调用 mach_msg
  ↓
成功接收大消息

总结

__CFRunLoopServiceMachPort 是 RunLoop 休眠的核心

  1. 准备消息头:设置接收端口和缓冲区
  2. 调用 mach_msg:线程阻塞等待 ⭐
  3. 被唤醒:检查返回值和 livePort
  4. 处理特殊情况:超时、消息过大、错误
  5. 返回结果:告诉调用者是哪个端口唤醒的

这就是 RunLoop "无事件时不消耗 CPU" 的秘密!

理财学习笔记(一):每个人必须自己懂理财

作者 唐巧
2026年1月24日 08:20

序言

我打算系统性整理一下这几年投学习投资理财的心得。因为一方面通过总结,可以让自己进一步加深对投资的理解。另一方面我也想分享给同样想学习理财的读者们。

我的女儿虽然还在读小学,但我也给她报了一个针对小学生的财商课。她对理财非常有兴趣,我也想通过这一系列的文章,给她分享她爸爸的理财成长经历。

这是这是本系列的第一篇,主题是每个人必须自己懂理财。

我身边的案例

我是 80 年代出生的,不得不说,我所处的是那个年代是缺乏理财和财商教育的。因此,我发现我身边的人大多不具备优秀的理财能力。

下面我举几个身边朋友的真实例子。

朋友 A:

他都把挣到的钱存银行定期或者余额宝。但是在现在这个年代,收益率是非常低的,只有一点几。但是他非常胆小,怕买其他的产品会导致亏损,所以说不敢碰。

朋友 B:

朋友 B 买了很多基金。但是他胆子很小,每个只买 1000 - 5000 块钱。然后账户里面有着几十只基金。既看不过来,也不知道应该如何操作。

唯一好的一面是:不管任何行业有行情,他都有一只基金命中。这让他的错失恐惧症(FOMO)小了很多。

朋友 C:

我这个朋友之前在快手上班,在 P2P 盛行的年代,把自己的所有积蓄都投在 P2P 上,最后爆雷,损失惨重。

朋友 D:

这个朋友通过另外一个朋友了解到有一个股票正在做庄阶段,未来会大涨,于是就听信买入,最后损失了 90%。

朋友 E:

朋友 E 的大学同学有一个在香港卖保险,于是听朋友的推荐在香港买了很多保险。但是过了 5 年,他发现收益率和最初承诺的相差非常大。这个时候看合同才发现,合同上写的收益测算并不保证。但是现在赎回的话,只能拿到非常少的本金,所以他只能继续硬着头皮每年交钱。

只有理解才能有效持有

听完上面几个朋友的故事,你身边有类似的朋友吗?

我跟一些朋友交流,我问他们,你们为什么不自己先学习投资理财的知识,之后再去做相关的操作呢?他们很多回答说,这个事情太专业了,专业的事情交给专业的人做就可以了。

当我反问他们:假如你买了一个专业人士管理的基金,那你对他的信仰来自于哪呢?你其实对他每个月发的报告并没有完全的判断能力,你只能选择相信他。

大多数时候,你其实相信的是他过去的业绩。如果它连续三年、连续五年一直都盈利,或者有超额收益,你就会持续持有它,甚至买入更多。

如果它连续几年亏损或者某一年大额亏损,你就会质疑它,甚至赎回它。

你的信心其实就是来源于过去的业绩表现。那这和散户的追涨杀跌有什么本质区别呢?

在你持仓持续下跌的那些时间,你能睡好觉吗?如果你不能理解它,那显然不能。

所以我说,每个人必须懂投资理财。

只有你深刻理解了你买入的是什么,才能在它下跌的时候有信心继续持有它,甚至抄底,才能睡得着觉。

小结

每个人都必须懂理财。因为银行的定期存款利率太低,而其他理财产品都需要深刻理解,才可能做到长期持有。

另外,社会上充斥着像 P2P 一类的产品,以及宣传这类产品的巧舌如簧的销售。他们不断地诱惑着我们,如果我们没有辨识能力,也可能将自己辛苦一辈子挣到的钱损失掉。

以上。

mach_msg_header_t详解

作者 iOS在入门
2026年1月23日 18:01

借助AI能力分析。

mach_msg_header_t - Mach 消息头

作用

这是 Mach 消息的头部结构,用于在 macOS/iOS 的进程间(或线程间)传递消息。

6个字段详解

typedef struct {
    mach_msg_bits_t      msgh_bits;         // 消息标志位
    mach_msg_size_t      msgh_size;         // 消息总大小(字节)
    mach_port_t          msgh_remote_port;  // 目标端口(收信人)
    mach_port_t          msgh_local_port;   // 本地端口(回信地址)
    mach_port_name_t     msgh_voucher_port; // 追踪端口(调试用)
    mach_msg_id_t        msgh_id;           // 消息ID(自定义)
} mach_msg_header_t;

形象比喻(信封):

字段 对应信封上的 说明
msgh_remote_port 收件人地址 消息发往哪个端口
msgh_local_port 回信地址 如果需要回复,发到这里
msgh_size 信件大小 包括信封和内容
msgh_bits 邮寄方式 挂号信、平信等
msgh_id 信件编号 用于区分不同类型的信
msgh_voucher_port 追踪单号 用于追踪和调试

在 RunLoop 中的使用

1. 发送唤醒消息(CFRunLoopWakeUp)

// 构造消息头
mach_msg_header_t header;
header.msgh_remote_port = rl->_wakeUpPort;  // 发往唤醒端口
header.msgh_local_port = MACH_PORT_NULL;    // 不需要回复
header.msgh_size = sizeof(mach_msg_header_t); // 只有头,无内容
header.msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, 0);
header.msgh_id = 0;

// 发送(唤醒 RunLoop)
mach_msg(&header, MACH_SEND_MSG, ...);

2. 接收消息(RunLoop 休眠)

// 准备缓冲区
uint8_t buffer[3 * 1024];
mach_msg_header_t *msg = (mach_msg_header_t *)buffer;

msg->msgh_local_port = waitSet;  // 在哪个端口等待
msg->msgh_size = sizeof(buffer);  // 缓冲区大小

// 阻塞等待(线程休眠)
mach_msg(msg, MACH_RCV_MSG, ...);

// 被唤醒后,检查消息来源
if (msg->msgh_local_port == _wakeUpPort) {
    // 手动唤醒
} else if (msg->msgh_local_port == _timerPort) {
    // 定时器到期
}

关键理解

mach_msg_header_t 是 Mach IPC 的核心

  1. 通信基础:所有 Mach 消息都以这个头开始
  2. 路由信息:指明消息的来源和去向
  3. RunLoop 休眠/唤醒:通过接收/发送消息实现

完整消息结构

┌──────────────────────┐
│ mach_msg_header_t    │ ← 消息头(必需)
├──────────────────────┤
│ 消息体(可选)        │ ← 实际数据
├──────────────────────┤
│ trailer(可选)       │ ← 附加信息
└──────────────────────┘

RunLoop 的简化消息:只有头部,无消息体(称为 "trivial message"),足以唤醒线程。

昨天 — 2026年1月23日iOS

Maintaining shadow branches for GitHub PRs

作者 MaskRay
2026年1月22日 16:00

I've created pr-shadow with vibecoding, a tool that maintains a shadow branch for GitHub pull requests(PR) that never requires force-pushing. This addresses pain points Idescribed in Reflectionson LLVM's switch to GitHub pull requests#Patch evolution.

The problem

GitHub structures pull requests around branches, enforcing abranch-centric workflow. There are multiple problems when you force-pusha branch after a rebase:

  • The UI displays "force-pushed the BB branch from X to Y". Clicking"compare" shows git diff X..Y, which includes unrelatedupstream commits—not the actual patch difference. For a project likeLLVM with 100+ commits daily, this makes the comparison essentiallyuseless.
  • Inline comments may become "outdated" or misplaced after forcepushes.
  • If your commit message references an issue or another PR, each forcepush creates a new link on the referenced page, cluttering it withduplicate mentions. (Adding backticks around the link text works aroundthis, but it's not ideal.)

These difficulties lead to recommendations favoring less flexibleworkflows that only append commits (including merge commits) anddiscourage rebases. However, this means working with an outdated base,and switching between the main branch and PR branches causes numerousrebuilds-especially painful for large repositories likellvm-project.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
git switch main; git pull; ninja -C build

# Switching to a feature branch with an outdated base requires numerous rebuilds.
git switch feature0
git merge origin/main # I prefer `git rebase main` to remove merge commits, which clutter the history
ninja -C out/release

# Switching to another feature branch with an outdated base requires numerous rebuilds.
git switch feature1
git merge origin/main
ninja -C out/release

# Listing fixup commits ignoring upstream merges requires the clumsy --first-parent.
git log --first-parent

In a large repository, avoiding rebases isn't realistic—other commitsfrequently modify nearby lines, and rebasing is often the only way todiscover that your patch needs adjustments due to interactions withother landed changes.

In 2022, GitHub introduced "Pull request title and description" forsquash merging. This means updating the final commit message requiresediting via the web UI. I prefer editing the local commit message andsyncing the PR description from it.

The solution

After updating my main branch, before switching to afeature branch, I always run

1
git rebase main feature

to minimize the number of modified files. To avoid the force-pushproblems, I use pr-shadow to maintain a shadow PR branch (e.g.,pr/feature) that only receives fast-forward commits(including merge commits).

I work freely on my local branch (rebase, amend, squash), then syncto the PR branch using git commit-tree to create a commitwith the same tree but parented to the previous PR HEAD.

1
2
3
4
5
6
Local branch (feature)     PR branch (pr/feature)
A A (init)
| |
B (amend) C1 "Fix bug"
| |
C (rebase) C2 "Address review"

Reviewers see clean diffs between C1 and C2, even though theunderlying commits were rewritten.

When a rebase is detected (git merge-base withmain/master changed), the new PR commit is created as a merge commitwith the new merge-base as the second parent. GitHub displays these as"condensed" merges, preserving the diff view for reviewers.

Usage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# Initialize and create PR
git switch -c feature
edit && git commit -m feature

# Set `git merge-base origin/main feature` as the initial base. Push to pr/feature and open a GitHub PR.
prs init
# Same but create a draft PR. Repeated `init`s are rejected.
prs init --draft

# Work locally (rebase, amend, etc.)
git fetch origin main:main
git rebase main
git commit --amend

# Sync to PR
prs push "Rebase and fix bug"
# Force push if remote diverged due to messing with pr/feature directly.
prs push --force "Rewrite"

# Update PR title/body from local commit message.
prs desc

# Run gh commands on the PR.
prs gh view
prs gh checks

The tool supports both fork-based workflows (pushing to your fork)and same-repo workflows (for branches likeuser/<name>/feature). It also works with GitHubEnterprise, auto-detecting the host from the repository URL.

Related work

The name "prs" is a tribute to spr, which implements asimilar shadow branch concept. However, spr pushes user branches to themain repository rather than a personal fork. While necessary for stackedpull requests, this approach is discouraged for single PRs as itclutters the upstream repository. pr-shadow avoids this by pushing toyour fork by default.

I owe an apology to folks who receiveusers/MaskRay/feature branches (if they use the defaultfetch = +refs/heads/*:refs/remotes/origin/* to receive userbranches). I had been abusing spr for a long time after LLVM'sGitHub transition to avoid unnecessary rebuilds when switchingbetween the main branch and PR branches.

Additionally, spr embeds a PR URL in commit messages (e.g.,Pull Request: https://github.com/llvm/llvm-project/pull/150816),which can cause downstream forks to add unwanted backlinks to theoriginal PR.

If I need stacked pull requests, I will probably use pr-shadow withthe base patch and just rebase stacked ones - it's unclear how sprhandles stacked PRs.

❌
❌