从6分钟到20秒-做照片编辑软件时的性能优化经验

前几天手搓了一个照片调色的小 App,还发了个视频出来。虽然这个视频没有人感兴趣,不过这个开发的过程中有一些点我觉得还是值得分享出来的。[视频链接]

这个 App 大致的功能就是:
这个主要的功能在开发的过程中也是遇到了一些坑,最终效果呢距离预期也有那么一点点差距。这个事儿之后找机会再分享吧。总之,做到了现在这个——效果上将将可以接受的程度。

我遇到的问题是什么呢?
说起来挺简单的哈,做的时候其实也不是特别难,没花多少时间就撸出来了。跑起来一看,妈耶~,我都惊呆了!!
跑起来之后这个功能是正常的,但是呢,一张普通的 4K 图片,四千乘三千那种,跑了有6分多钟。对,我没说错,是6分钟,超过360秒。

虽然时间是挺长的,但是至少它是自动化的对不对?比一直用手扒拉(bu lu)那小滑块也还是方便了不少的,是吧?

我当然是不会就这样善罢甘休的,就开始了一些小小的优化。最终,达到了一张 4K 图片可以在 20秒左右处理完成,这基本上就达到了一个可以接受的程度。最终效果可以看我之前发的那个[视频],就是那个样子。

接下来就是比较技术的内容了,稍微分享一下这次优化的思路和技术实现。

我首先做的一件事就是把可以预处理的部分提取出来,我用来传给模型的参数里有一些是通过对原图片的颜色和直方图进行运算得出的数据,所以这些对原图的运算是可以提前到图片载入的时候就提前进行运算的。当然,这个操作只是给运算压力换一个位置,并没有解决问题。然而通过遍历图片所有像素来做这些运算是非常非常耗时的。

然后我就跟 GPT 沟通了一下,怹给了我一些建议,可以试试 Accelerate 这个库。
Accelerate 这个是非常强大的,专门为大规模数学运算和图像运算设计的一个库。而且苹果的文档也是很给力的,有针对具体使用场景提供的文档

我对预处理部分由该后的代码片段:

分离 RGB 各个通道
vImageConvert_ARGB8888toPlanar8

var sourceBuffer = try! vImage_Buffer(cgImage: cgImage)
var rChannel = [UInt8] (repeating: 0, count: pixelCount)
var gChannel = [UInt8] (repeating: 0, count: pixelCount)
var bChannel = [UInt8] (repeating: 0, count: pixelCount)
var aChannel = [UInt8] (repeating: 0, count: pixelCount)

rChannel.withUnsafeMutableBytes { rChannelPointer in
   gChannel.withUnsafeMutableBytes { gChannelPointer in
       bChannel.withUnsafeMutableBytes { bChannelPointer in
           aChannel.withUnsafeMutableBytes { aChannelPointer in
               var redBuffer = vImage_Buffer(data: rChannelPointer.baseAddress, height: vImagePixelCount(height), width: vImagePixelCount(width), rowBytes: Int(width))
               var greenBuffer = vImage_Buffer(data: gChannelPointer.baseAddress, height: vImagePixelCount(height), width: vImagePixelCount(width), rowBytes: Int(width))
               var blueBuffer = vImage_Buffer(data: bChannelPointer.baseAddress, height: vImagePixelCount(height), width: vImagePixelCount(width), rowBytes: Int(width))
               var alphaBuffer = vImage_Buffer(data: aChannelPointer.baseAddress, height: vImagePixelCount(height), width: vImagePixelCount(width), rowBytes: Int(width))

               // 分离 RGB 通道
               vImageConvert_ARGB8888toPlanar8(&sourceBuffer, &redBuffer, &greenBuffer, &blueBuffer, &alphaBuffer, vImage_Flags(kvImageNoFlags))
           }
       }
   }
}

计算各个通道的直方图
vImageHistogramCalculation_ARGB8888

var buffer = try! vImage_Buffer(cgImage: image)
        
var histogramRed = [vImagePixelCount](repeating: 0, count: 256)
var histogramGreen = [vImagePixelCount](repeating: 0, count: 256)
var histogramBlue = [vImagePixelCount](repeating: 0, count: 256)
var histogramAlpha = [vImagePixelCount](repeating: 0, count: 256)

histogramRed.withUnsafeMutableBufferPointer { zeroPtr in
   histogramGreen.withUnsafeMutableBufferPointer { onePtr in
       histogramBlue.withUnsafeMutableBufferPointer { twoPtr in
           histogramAlpha.withUnsafeMutableBufferPointer { threePtr in
               
               var histogramBins = [zeroPtr.baseAddress, onePtr.baseAddress,
                                    twoPtr.baseAddress, threePtr.baseAddress]
               
               histogramBins.withUnsafeMutableBufferPointer { histogramBinsPtr in
                   // `buffer` is a `vImage_Buffer` structure.
                   _ = vImageHistogramCalculation_ARGB8888(&buffer,
                                                           histogramBinsPtr.baseAddress!,
                                                           vImage_Flags(kvImageNoFlags))
               }
           }
       }
   }
}

数据归一化
vDSP_normalize

let floatChannelData = channel.map { Float($0) }
        
// 计算均值和标准差
var mean: Float = 0
var stdDev: Float = 0
vDSP_normalize(floatChannelData, 1, nil, 1, &mean, &stdDev, vDSP_Length(floatChannelData.count))

用了这个工具,直接让我的数据预处理部分的耗时从分钟级下降到了毫秒级。

接下来就到了重新组织数据的部分,这部分也是所有环节中最耗时的一个。之前用的是传统的方法,把像素数据放到数据数组里之后,遍历之前预处理的数据,一个一个复制到这个数据数组里。这无疑就是在逐像素遍历的基础上又在每个图块都又遍历了一遍额外的数据,这个预处理数据也有上百个元素呢,就是一个相当沉重的负担了。

这部分的优化主要做了两件事,一个是隐性的,把像素写入数据数组的代码尽量写成对并行友好的形式,这样的话,一方面编译器的优化能力可以发挥一些作用,能在一定程度上提高多核的并行度;另一方面以后可以再用多线程去进一步优化。

nonisolated func copyBlockDataToMLArray(pixelData: [UInt8], mlArray: MLMultiArray, blockX: Int, blockY: Int, blockSize: Int, blockIndex: Int, width: Int, height: Int, bytesPerPixel: Int) {
        
   for ih in 0..<blockSize {
      for iw in 0..<blockSize {
          let x = blockX * blockSize + iw
          let y = blockY * blockSize + ih
          
          let isOut = x >= width || y >= height
          
          let pixelIndex = MyHelper.getIndexFromXY(x: x, y: y, width: width) * bytesPerPixel
          
          let r = isOut ? -1 : Double(pixelData[pixelIndex])
          let g = isOut ? -1 : Double(pixelData[pixelIndex + 1])
          let b = isOut ? -1 : Double(pixelData[pixelIndex + 2])
          
          let di = MyHelper.getIndexFromXY(x: iw, y: ih, width: blockSize)
          let batchIndex = blockIndex * inputWidth + di
          
          mlArray[batchIndex    ] = NSNumber(value: r)
          mlArray[batchIndex + 1 * blockSize * blockSize] = NSNumber(value: g)
          mlArray[batchIndex + 2 * blockSize * blockSize] = NSNumber(value: b)
          
      }
   }
}

另外一件事,是用 Swift 的指针操作直接将预处理数据拷贝到数据数组,时间复杂度直接从O(N) 变成 O(1) 。

当然,Swift 的指针没有 C++ 那么自由,写起来也需要适应一下。但是至少它可以用,并且是好用的。

for ibh in 0..<bh {
   for ibw in 0..<bw {

       copyBlockDataToMLArray(pixelData: pixelData, mlArray: inputArr, blockX: ibw, blockY: ibh, blockSize: blockSize, blockIndex: blockedCount, width: width, height: height, bytesPerPixel: bytesPerPixel)
       
       // 将遍历赋值改为整块内存拷贝,速度大幅提升,4K图片从6分钟缩短为20秒
       let extraDataHeadIndex = blockedCount * inputWidth + blockSize * blockSize * 3
       inputArr.withUnsafeMutableBufferPointer(ofType: Double.self) { inputArrPtr, _ in
           extraDatas.withUnsafeBufferPointer { extraDataPtr in
               inputArrPtr.baseAddress!
                   .advanced(by: extraDataHeadIndex)
                   .update(from: extraDatas, count: extraDatas.count)
           }
       }
       
       blockedCount += 1

       // .......
       // 其他代码
   }
}

最后的部分就是将模型输出的数据重新拼接成图片,这里的思路是,每一个图块写到图片的像素缓存里可以没有固定的顺序,完全是可以并行执行的。

所以我就用了一个线程池,(所谓的线程池就是一个 OperationQueue),让每一个图块的写入任务并行起来,这块儿最终也提升了一些运行时间。

let fullOutputQueue = OperationQueue()
fullOutputQueue.maxConcurrentOperationCount = 10

// ......
// 其他代码

fullOutputQueue.addOperation {
   // outPixelData 应该保持弱引用,暂时不管警告
   self.copyMLArrayToOutputArray(input: result, output: &outPixelData, width: width, height: height, blockSize: self.blockSize, blockWidthCount: bw, blockedCount: _blockedCount, blockStartIndex: blockStartIndex, bytesPerPixel: bytesPerPixel)
}

最终,效果就像我之前发的[视频]里的那样,可以在20秒左右的时间处理完成一张 4K 的照片。达到了一个基本可用的状态。

最后总结一下关键点:
1. 使用 Accelerate 库优化大规模运算
2. 整块拷贝内存
3. 多线程并行处理

Xcode 16.1
Swift 5

视频版

Gitlab 配置自定义 clone 地址

前情提要

  • 我是通过 docker 安装的 gitlab_ce ,版本 14.4.2。因为机器还要跑其他服务,所以不能使用 80、22 等端口,需要映射到其他端口。
  • 通过 gitlab.rb 配置文件配置的 external_url 只能配置在 clone 标签下得 host,不能带有端口号。

Clone with HTTP

  1. 首先必须是 Admin 账号。
  2. 进入 Admin Area。
  3. 在 Settings 的 General 中选择 Visibility and access controls。
  4. 往下拉,有一个标题为 Custom Git clone URL for HTTP(S) 的输入框。
  5. 可以在输入框中输入带端口号的 url。
  6. 然后保存就 OK 了。

Clone with SSH

  1. 需要在 gitlab.rb 中配置。
  2. 搜索 gitlab_ssh_host。
    // 配置主机地址 
    gitlab_rails['gitlab_ssh_host'] = '192.168.72.90'
  3. 然后搜索 ssh_port。
    // 配置主机的 ssh 端口 
    gitlab_rails['gitlab_shell_ssh_port'] = 40022
  4. 然后保存,重启 gitlab,就完成了。

Windows 编译 AOSP 趟坑笔记

  • 首先 AOSP 是不支持在 windows 系统下直接编译的,目前官方也不再支持 Mac OS ,所以只能搞个 Ubuntu 环境。
  • 我首先尝试了 wsl ,安装 Microsoft Store 官方的 Ubuntu 。这条路实际上并没有走通,不过我还是分享一些关键点。

首先,要保证代码所在文件夹必须是大小写敏感的。使用管理员模式运行命令行,执行下面命令,开启文件夹的大小写敏感。

fsutil.exe file setCaseSensitiveInfo E:\xxx\aosp enable

这里要注意一点,建议先处理好文件夹再下载代码,因为这条命令只能操作一层文件夹,如果已经下载完的代码,再想改变所有文件夹的大小写敏感状态就需要写脚本去处理了,会很慢。

其次,Ubuntu 要切换到 wsl2 ,具体方法可以搜索。

然后按照官方文档的方法安装环境和同步代码,如果访问官方代码库速度不理想也可以用国内清华或者科大的镜像,具体操作在他们镜像站都有。

访问 windows 文件夹的话,盘符在 /mnt 文件夹下。

我因为代码下载到了 windows 路径中,导致无法启动编译。会一直报系统不支持的错误:

Failed to listen for path logs: listen unix 
out/.path_interposer_log: bind: operation not supported

已经提 issue
https://github.com/microsoft/WSL/issues/7283


现在切回到主线,我后来尝试了使用 docker 。可以成功的启动编译了,但是访问 windows 路径会及其慢,一次编译用了10几个小时(配置:i7 + 32G 内存)。

  • windows 下不论是 wsl 直接安装的 Ubuntu 还是用 docker,都不建议直接访问 windows 路径。
  • 在 Ubuntu 下直接同步代码,按照官方文档进行编译,可以很顺利的完成。
  • 不过如何从 docker 启动模拟器我目前还没找到好办法,可能会考虑电脑直接装双系统了。

解决 iOS 的 SSH 连接不上的问题

要用 SSH 连接 iPhone 手机,当然要越狱才行。当然越狱不是这篇的重点,就不多说了。
然后手机需要装有 OpenSSH ,通过 Cydia 就可以安装,很简单。
然后就遇到问题了,用的是一个 iPhone 6,系统 10.3.3 。发现即使都安装好了,使用 SSH 还是无法连接。

查资料的过程就不介绍了,解决方案如下:
首先需要在手机上安装一个命令行 App ,自己找 Cydia 的源就好了,只要能用就行。
使用命令行先停掉 sshd 的 plist

launchctl unload /Library/LaunchDaemons/com.openssh.sshd.plist

然后用如下命令启动 sshd

/usr/libexec/sshd-keygen-wrapper

之后就可以在电脑上使用 ssh 操作我的苹果手机了。
没有在其他设备和系统测试过,不知道能不能解决所有问题,希望对看到这篇博文的你能有一点帮助。

Unity3D Custom Render Texture Shader 编译到低版本 Shader model 环境

写好的 Custom Render Texture 的 shader 在打包的时候报出了一个错误:

[Unity] Player export failed. Reason: Shader error in 'Hidden/MyCustomRenderTextureShader': 'asuint' : no matching overloaded function found at ***/CGIncludes/UnityCustomRenderTexture.cginc(189) (on gles)

查资料发现 asuint 这个函数是在 Shader Model 4 (SM4) 才开始支持。
然后稍微看了一下 cginc 里边的代码,发现只有一个地方用到了这个函数——是把浮点类型的 primitive ID 转成 uint 类型。

OUT.primitiveID = asuint(CustomRenderTexturePrimitiveIDs[primitiveID]);

这个 primitiveID 是 update zone 的当前正在处理的索引,而我目前并没有使用很多 update zone 来实现效果的需求,所以决定尝试一下,直接将 float 类型强转成 uint 。

OUT.primitiveID = (uint)(CustomRenderTexturePrimitiveIDs[primitiveID]);

操作步骤:

  • 在自己的工作目录下创建一个自己的 CustomRenderTexture 的 cginc ,也就是新建个文本文件,把后缀改成 .cginc。
  • 把 UnityCustomRenderTexture.cginc 中的代码全部复制到自己新建的 cginc 中,并且按照上面代码修改。
  • 别忘了修改一下头部的宏定义名称 #ifndef UNITY_CUSTOM_TEXTURE_INCLUDED 跟默认的做一个区分。
  • 在 shader 代码中引用我们自己的 cginc ,就可以用了。

Unity3D UGUI 基础布局

play-sharp-fill

1. 轴点 (Pivot)
作为控件的“轴”,在控件状态改变时对最终布局产生影响。比如,旋转的时候控件就以轴点为中心旋转,调整尺寸或缩放的时候轴点会固定在原位置。

2. 锚点 (Anchor)
Anchor 看起来的样子是四个相对的三角形。Anchor 决定了当前控件与父控件的位置关系。控件的 Rect Transform 的坐标 Pos X 和 Pox Y 就以 Anchor 为原点。也就是 Pivot 会放在 Anchor + (Pos X, Pox Y) 的位置上。当父控件状态(旋转、缩放、移动等)发生变化时,控件会保持这个关系跟随父控件。
在 Anchor 选择面板,按住 Shift 选择会同时将 Pivot 设置在 Anchor 选择的相同的位置;按住 Alt 选择会将控件位置同时停靠在 Anchor 位置。如果要同时将 Pivot 和位置放在 Anchor 点,就同时按住 Shift 和 Alt。

3. 尺寸自适应
也可以选择自动伸展的 Anchor 比如,全屏或者 X 轴或 Y 轴方向自动伸展。这个时候在 Rect Transform 中的坐标或(和)尺寸设置会变成 Left Right Top Bottom 。这些值是相对 Anchor 自动伸展方向的距离。这个概念是非常有用的,可以实现控件相对父控件同比例缩放。具体效果看我撸的视频会比较清楚一点。

Unity3D 解决默认碰撞检测的缺陷 —— 实现理想的匀速直线运动

首先介绍一下默认碰撞检测存在的问题。先不提【我只想要你丫告诉我碰撞盒撞上了,却非得要我挂个钢体组件,还得接受重力,阻尼,角速度的影响。不是在逗我?】,那么我们手动勾选上钢体的 Is Kinematic 属性,自己来控制。
我们看下边的图:
collider1
当这个圆走到实线圆圈位置的时候,就会触发碰撞检测回调。当然,如果是在虚线圆圈的位置,回调是不会被触发的。那么问题来了,如果一帧的移动距离刚好是从虚线位置移动到实线位置,情况就是这样的,移动前一帧不会触发碰撞回调,而后一帧两个碰撞体已经嵌到一起了。
当然,如果只是小小的插入一下,也并没有什么问题,接下来看下面的图:
collider2
当一帧的移动距离超过碰撞目标的时候,奇迹发生了,这时碰撞检测的回调根本不会触发。也就是说在速度足够快的情况下,你的墙是挡不住你的球的。
为了实现一个在理想条件下的匀速直线运动,我就用 Unity 本身的射线检测功能来解决这个问题。
首先取出自己的碰撞盒

var selfCollider = GetComponent<Collider2D>();
Collider 中有 Raycast 和 Cast 两个方法,参数差不多,具体的区别,还得画两张图:
ray1
图一
ray2
图二
ray3
图三
图一、图二中蓝色的线就是 Raycast 的结果,是从自己的中心射到目标碰撞盒上,如果按照这个射线的距离运动,结果就是图二的样子。
图三种的蓝色线是 Cast 方法的结果,射到目标碰撞盒上的是自己碰撞盒与目标碰撞盒的交点。这个距离就是从当前点移动到发生碰撞的点的距离。
简单一点就用 Cast 方法就够了,Raycast 比 Cast 提供了更多细节操作,比如专门针对某一层的检测等,如果需要做更细节的优化,就得自己用 Raycast 方法来处理。

RaycastHit2D[] casthits = new RaycastHit2D[maxRaycastCache]; 
int num = selfCollider.Cast(forward, casthits);
RaycastHit 中比较重要的属性:
collider —— 目标碰撞盒
distance —— 到碰撞点的距离(上面图3中蓝线的长度)
normal —— 碰撞点的法线
point —— 目标碰撞点
接下来的逻辑就是:
比较本帧移动距离和 distance ,如果本帧移动距离比较小,就直接移动。

float stepDistance = speed * Time.fixedDeltaTime; 
if (stepDistance < distance) 
{ 
   gameObject.transform.Translate(forward * stepDistance); distance -= stepDistance; 
}
当然之后要把总距离减去这步的移动距离。
如果这一帧的距离大于剩余的距离了,就需要处理反射了。
先移动到碰撞点→方向向量反射→继续移动完。

while (stepDistance > nextDistance) 
{ 
    gameObject.transform.Translate(forward * nextDistance); 
    stepDistance -= nextDistance;

    forward = Vector2.Reflect(forward, normal); 
    /* 找下一个碰撞目标 */ 
    …… 
}
打完收工。
当然这只是简单的描述,实际上还会遇到一些坑,比如当前自己和一个碰撞盒有重叠的时候,取到的第一个 RaycastHit 永远是有接触的这个碰撞盒,此时与方向向量无关,所以需要根据自己的逻辑处理。还有同时碰撞两个或多个碰撞盒等,都要小心处理。

libgit2使用教程(十三)git rebase

rebase 就是将制定目标分支,或者制定 commit 所在的一条路径,直接插到当前分支或目标分支。(好像有点乱,具体的东西自己去查吧)

简单点说,这不是合并。

当然,虽然不是 merge 但是也会有冲突的可能,所以中途有解决冲突的需要,所以 rebase 操作是分阶段进行的,因此逻辑会复杂一些。

首先尝试打开现有未完成的 rebase

git_rebase* prebase = nullptr; 
git_rebase_options rebase_opt = GIT_REBASE_OPTIONS_INIT; 
error = git_rebase_open(&prebase, rep, &rebase_opt);

如果存在未完成的 rebase,可以选择继续将其完成,或者把它终止掉

git_rebase_abort(prebase);

在 git_rebase_open 返回 -3 (也就是 GIT_ENOTFOUND ) 表示当前仓库并没有其他未完成的 rebase 可以放心大胆的从头开始搞。

接下来创建一个 rebase

git_rebase* prebase = nullptr; 
git_rebase_options rebase_opt = GIT_REBASE_OPTIONS_INIT; git_reference* onto_branch = nullptr; 
git_annotated_commit* onto = nullptr; 
git_branch_lookup(&onto_branch, rep, "new", GIT_BRANCH_LOCAL); git_annotated_commit_from_ref(&onto, rep, onto_branch); 
git_rebase_init(&prebase, rep, 
   nullptr /* current branch */, 
   nullptr /* upstream */ , 
   onto    /* branch to rebase onto */, 
   &rebase_opt);

init 的第三个参数是需要被操作的分支,传空表示当前分支;第四个参数和第五个参数这俩二选一,前者表示以一个 commit 为节点把到这个节点为止的一条链合并到目标分支;后者是直接选一个分支的最新一个 commit ,将这个分支的整条链合并到目标分支。

当然,这个时候工作区不会有任何变化。到 .git 文件夹里面会看到多了一个叫做 rebase-merge 的文件夹。如果这个时候程序被终止,这个文件夹会保留,在下一次启动的时候,就可以通过 git_rebase_open 打开这个 rebase 。

接下来就是实际执行 rebase 这个操作

git_rebase_operation* operation = nullptr; 
git_rebase_next(&operation, prebase)

这里可能存在遍历,但是为什么会有多个 operation 我也还没搞太明白,不过为了避免出事,还是循环调用保险一些:

while (git_rebase_next(&operation, prebase) != GIT_ITEROVER)

接下来,不要忘记查看是否有冲突,需要将冲突解决才可以做后边的 commit 的操作:

// reslove conflicts 
git_repository_index(&index, rep); 
if (git_index_has_conflicts(index)) 
{ 
   // git checkout --theirs 
   git_checkout_options opt = GIT_CHECKOUT_OPTIONS_INIT; 
   opt.checkout_strategy |= GIT_CHECKOUT_USE_THEIRS; 
   git_checkout_index(rep, index, &opt); 
} 

// add 
git_index_update_all(index, nullptr, nullptr, nullptr); 
git_index_write(index);

在解决完冲突,并且 add 之后,就可以 commit 了。这里并不需要使用 commit 的 create 接口,rebase 部分提供了 rebase 专用的 commit 接口。

git_signature* me = nullptr; 
git_oid new_commit; 
git_signature_now(&me, "XiaochenFTX", "xiaochenftx@gmail.com"); 
git_rebase_commit(&new_commit, prebase, me, me, "UTF-8", "new rebase");

最后,做一下收尾 finish 掉这个 rebase 操作

git_rebase_finish(prebase, me);

示例代码:sample13

libgit2使用教程(十二)git tag

tag 的作用是作为重要节点的标记,在需要的时候可以直接切过去。所以在 git 管理系统,比如 github ,直接以带信息描述的 tag 作为 release。并且通常也习惯以版本编号作为 tag 名。

首先来看看仓库里有哪些 tag ,libgit2 提供了两个接口,它们有不同的作用。
1. list

git_strarray tags = {0};
git_tag_list(&tags, rep);
for (int i = 0; i < tags.count; ++i)
{
   std::cout << "tag: " << tags.strings[i] << "\n";
}

输出的是 tag 的名字,并且没有更多的详细信息。
2. foreach

int tag_foreach_cb(const char *name, git_oid *oid, void *payload) 
{
    git_repository *rep = (git_repository *) payload;
    git_tag *tag = nullptr;
    
    int error = 0;
    std::cout << "tag name: " << name << "\n";
    error = git_tag_lookup(&tag, rep, oid);
    if (error < 0) {
        const git_error *e = giterr_last();
        std::cout << "Error: " << error << " / " << e->klass << " : " << e->message << std::endl;
        return 0;
    }
    std::cout << "tag message: " << git_tag_message(tag) << "\n";
    
    git_tag_free(tag);
    
    return 0;
}
git_tag_foreach(rep, tag_foreach_cb, rep);

这个接口是通过回调的方式遍历所有的 tag。回调中的参数 name 返回的是 tag 的完整路径(refs/tags/tagname 这种形式,这样看实际上它也是一种 reference),这一点要特别注意;oid 是这个 tag 的 oid,如果要获取这个 tag 对象的指针,则需要使用 lookup 这个接口去获取——这个接口需要仓库指针作为参数,所以仓库指针需要通过 payload 传入。lookup 的时候有可能会出现找不到的情况,那是因为只有带描述的 tag 才能被找到,也就是说只有名字的轻量级 tag 是找不到的。

然后有必要解释一下什么是轻量级 tag 么?
轻量级 tag 是这个 tag 只有名字,没有其他信息。通过 git tag [tagname] 这个命令来创建;
除了轻量级 tag 之外还有一种就是带附注的 tag,这个 tag 要求创建的时候输入一段描述信息,通过 git tag -a [tagname] -m [message] 这个命令创建。

因此使用 libgit2 创建 tag 也有不同的接口来实现。
1. 轻量级 tag

git_reference *head = nullptr;
git_annotated_commit *commit = nullptr;
git_object *target = nullptr;
git_oid tag_oid;
git_repository_head( &head, rep);
git_annotated_commit_from_ref( &commit, rep, head);
git_object_lookup( &target, rep, git_annotated_commit_id(commit), GIT_OBJ_COMMIT);
error = git_tag_create_lightweight(&tag_oid, rep, "v1.2.3", target, false);
if (error == GIT_EEXISTS)
{
    std::cout<< "tag 123 already exists \n";
}

因为 tag 是一种 reference 所以,它一定是以 commit 为节点的,所以 target 参数要找一个目标 commit。

2. 带附注的 tag

git_reference *head = nullptr;
git_annotated_commit *commit = nullptr;
git_object *target = nullptr;
git_oid tag_oid;
git_signature *me = nullptr;
git_repository_head( &head, rep);
git_annotated_commit_from_ref( &commit, rep, head);
git_object_lookup( &target, rep, git_annotated_commit_id(commit), GIT_OBJ_COMMIT);
git_signature_now( &me, "XiaochenFTX", "xiaochenftx@gmail.com");
error = git_tag_create(&tag_oid, rep, "v3.2.1", target, me, "message", false);
if(error==GIT_EEXISTS)
{
    std::cout<< "tag 321 already exists \n";
}

相比 lightweight 版本多了两个参数,一个是提交者的信息,另一个是这个 tag 附带的信息。这个信息在 github 的 release 中会对外显示。

下面介绍一个模糊搜索的功能,对应的是 git tag -l “*.*” 这个命令:

git_strarray tags = {0};
git_tag_list_match(&tags, "v0.*", rep);
for (int i = 0; i < tags.count; ++i)
{
   std::cout << "tag: " << tags.strings[i] << "\n";
}

接下来是删除 git tag -d [tagname]

error = git_tag_delete(rep, "v1");

如果 error 返回的是 -3 也就是 GIT_ENOTFOUND 这个宏,表示并没有找到输入的这个 tag。

接下来,tag 作为一个标记,就是为了作为一个目标点可以直接切过去。使用命令行工具的命令是 git checkout [tagname],它实现的是将当前 head 切成一个空分支,这个分支指向的就是目标 tag。
然而,在代码中这个操作并不是通过 checkout 系列的接口实现,我们的目标是把对象换到 head 所以,需要的是设置当前的 head。

git_repository_set_head(rep, "refs/tags/v3.2.1");

这里的重点是第二个参数,需要这个 tag 的 reference 形式的全名,在 foreach 的回调中可以获取到,在文件目录里也可以找得到。

最后,要将 tag 上传到远端仓库,需要在 push 的时候显示的指出。就像命令行:git push origin –tags。在代码中的重点就是 push 接口的 refspecs 参数,由这个参数指定 push 什么东西。

git_push_options push_opts = GIT_PUSH_OPTIONS_INIT;
push_opts.callbacks.credentials = cred_acquire_cb;
const char *refs[] = {"refs/tags/*:refs/tags/*"};
git_strarray strarr = {(char **) refs, 1};
git_remote_lookup(&remote, rep, "origin");
git_remote_push(remote, &strarr, &push_opts);

示例代码:sample12

libgit2使用教程(十一)git push

将本地分支上传到远端,如果前面的各个功能都掌握了的话,这个接口可以算是非常简单的了。

需要注意的细节就是 1. 身份验证;2. refspecs

验证证书的回调,是通过 git_push_options 这个结构传递的,与之前一样的 callbacks 参数。详情可以参考前面的

int cred_acquire_cb(git_cred **cred, const char *url, const char *username_from_url, unsigned int allowed_types,
                    void *payload)
{
    git_cred_ssh_key_new(cred, username_from_url, nullptr, "/Users/xiaochen/.ssh/id_rsa", nullptr);
    return 0;
}
git_push_options opts = GIT_PUSH_OPTIONS_INIT;
opts.callbacks.credentials = cred_acquire_cb;

refspecs 是用来指定这个 push 操作的本地分支和远端分支。它的格式是本地分支远端目标分支中间以冒号隔开。

比如我要把本地的 master 推到远端,碰巧远端对应的分支也叫 master,那好,它就是这个样子的:
refs/heads/master:refs/heads/master

当然,我们必须制定远端目标,因为它不一定仅仅是 origin:

git_remote *remote = nullptr;
git_remote_lookup(&remote, rep, "origin");

那么最终,push 操作的代码就是:

const char *refs[] = {"refs/heads/master:refs/heads/master"};
git_strarray strarr = {(char **) refs, 1};
opts.callbacks.credentials = cred_acquire_cb;
git_remote_push(remote, &strarr, &opts);

示例代码:sample11

RSS
Follow by Email
YouTube
YouTube
Pinterest
fb-share-icon
LinkedIn
Share
VK
Weibo
WeChat
WhatsApp
Reddit
FbMessenger
Copy link
URL has been copied successfully!