阅读视图

发现新文章,点击刷新页面。

git branch Command: Create, List, and Delete Branches

Branches are how Git keeps your work separated from the main line of development. A new branch lets you experiment, fix a bug, or build a feature without touching what is already shipped, and a merge brings the work back together when it is ready. The tool that creates, lists, and removes those branches is git branch.

This guide explains how to use git branch to manage local and remote branches, filter by merged or upstream state, rename branches, and clean up stale ones.

git branch Syntax

The general form of the command is:

txt
git branch [OPTIONS] [BRANCH] [START_POINT]

With no arguments, git branch lists your local branches and marks the currently checked-out one with an asterisk. With a branch name, it creates a new branch pointing at the current commit. With additional flags, it can list remote branches, delete branches, rename them, or set upstream tracking.

Listing Branches

To list the local branches in the current repository, run git branch with no arguments:

Terminal
git branch
output
 develop
* main
feature/login

The asterisk marks the branch you are currently on. The branches are sorted alphabetically.

To include remote-tracking branches, add -a (all):

Terminal
git branch -a
output
 develop
* main
feature/login
remotes/origin/HEAD -> origin/main
remotes/origin/develop
remotes/origin/main

Anything prefixed with remotes/ is a local reference to a branch on a remote, not a branch you can commit to directly. Check it out to create a local tracking branch.

To list only remote-tracking branches, use -r:

Terminal
git branch -r

To see the latest commit on each branch, add -v:

Terminal
git branch -v
output
 develop a1b2c3d Add release notes
* main 7e8f9a0 Merge pull request #42
feature/login 3d4e5f6 Stub login form

For even more detail, -vv also prints the upstream branch each local branch tracks and whether it is ahead, behind, or in sync:

Terminal
git branch -vv
output
 develop a1b2c3d [origin/develop] Add release notes
* main 7e8f9a0 [origin/main] Merge pull request #42
feature/login 3d4e5f6 [origin/feature/login: ahead 2] Stub login form

Creating a Branch

To create a new branch, pass the name as an argument. This creates the branch at the current HEAD but does not switch to it:

Terminal
git branch feature/login

To start the branch from a specific commit, tag, or another branch, pass it as the second argument:

Terminal
git branch hotfix v1.4.0
Terminal
git branch hotfix 3d4e5f6

Creating a branch without switching to it is useful when you want to mark a commit for later work. Most of the time, though, you create a branch and switch to it in one step. The modern command for that is git switch -c:

Terminal
git switch -c feature/login

The older equivalent is git checkout -b feature/login. Both create the branch at the current commit and check it out immediately. See the create and list Git branches guide for a fuller walkthrough.

Renaming a Branch

To rename the branch you are currently on, use -m with the new name:

Terminal
git branch -m new-name

To rename any branch, pass both the old and the new name:

Terminal
git branch -m old-name new-name

If a branch with the target name already exists, this form fails to protect you from overwriting it. To force the rename and overwrite the existing branch, use the capital -M instead. Use -M with care: it discards the destination branch.

After renaming a branch that is already pushed, push the new name to the remote and delete the old one so collaborators see the change. The rename a Git branch guide walks through the full local-and-remote flow.

Deleting a Branch

To delete a local branch that has been merged into the current branch, use -d:

Terminal
git branch -d feature/login
output
Deleted branch feature/login (was 3d4e5f6).

Git refuses to delete a branch that has unmerged work, to protect you from losing commits:

output
error: The branch 'feature/wip' is not fully merged.
If you are sure you want to delete it, run 'git branch -D feature/wip'.

If you are sure the work is no longer needed, force the deletion with -D:

Terminal
git branch -D feature/wip

You cannot delete the branch you are currently on. Switch to another branch first, then delete it. For remote branch deletion, see the delete local and remote Git branch guide.

Filtering by Merged State

git branch can filter branches by whether they are fully merged into the current branch. List branches that have been merged:

Terminal
git branch --merged

This is the safest way to find branches that can be deleted without losing history. Before deleting anything, preview the filtered list:

Terminal
git branch --merged | grep -vE '^\*|^[[:space:]]*(main|develop)$'

The grep filter keeps the current branch and the protected main and develop branches out of the list. If the output looks correct, pipe the same list to git branch -d:

Terminal
git branch --merged | grep -vE '^\*|^[[:space:]]*(main|develop)$' | xargs -r git branch -d

To list branches that still have unmerged work, use --no-merged:

Terminal
git branch --no-merged

These are the branches you should review before deleting, because they contain commits that are not on your current branch.

Filtering Remote Branches Too

--merged and --no-merged accept a reference argument. To check against main from the remote:

Terminal
git branch --merged origin/main

This is useful in CI or cleanup scripts where you want to see which local branches are safely merged into the shipped code, regardless of which branch you are currently on.

Setting an Upstream Branch

An upstream branch tells Git which remote branch your local branch tracks for git pull and git push. To set it on an existing branch, use --set-upstream-to (or -u):

Terminal
git branch --set-upstream-to=origin/main main
output
Branch 'main' set up to track remote branch 'main' from 'origin'.

To remove tracking information from a branch, use --unset-upstream:

Terminal
git branch --unset-upstream main

When you push a new branch to a remote for the first time, pass -u to git push to create the remote branch and set it as the upstream in one step:

Terminal
git push -u origin feature/login

Showing the Current Branch

To print just the name of the branch you are on, use --show-current:

Terminal
git branch --show-current
output
main

This is the right command for shell prompts and scripts. It prints nothing if you are in a detached HEAD state, which scripts can detect and handle.

Copying a Branch

git branch -c copies a branch, including its reflog and configuration, to a new name:

Terminal
git branch -c old-branch new-branch

The original branch is kept. Use the capital -C to force the copy and overwrite an existing target branch. Copying is less common than creating a fresh branch, but it is useful when you want to preserve the history and config of a branch under a new name.

Sorting the Branch List

By default, branches are listed alphabetically. The --sort option changes the order. To sort by the date of the most recent commit:

Terminal
git branch --sort=-committerdate

The minus sign reverses the order, so the most recently updated branch appears first. This is handy when you return to a project after a break and want to see what you were working on last.

Quick Reference

Command Description
git branch List local branches
git branch -a List local and remote-tracking branches
git branch -r List only remote-tracking branches
git branch -v Show the latest commit on each branch
git branch -vv Show upstream tracking and ahead/behind counts
git branch NAME Create a new branch at HEAD
git branch NAME START Create a branch at a specific commit or tag
git branch -m NEW Rename the current branch
git branch -m OLD NEW Rename another branch
git branch -d NAME Delete a merged branch
git branch -D NAME Force-delete a branch with unmerged work
git branch --merged List branches merged into the current branch
git branch --no-merged List branches with unmerged work
git branch -u origin/main Set the upstream of the current branch
git branch --show-current Print the current branch name
git branch --sort=-committerdate Sort by most recent commit

FAQ

What is the difference between git branch and git checkout?
git branch manages branches: creating, listing, renaming, and deleting. git checkout (and its modern replacement git switch) changes which branch you are on. Creating and switching in one step uses git switch -c or git checkout -b.

How do I delete a remote branch?
git branch -d only deletes local branches. To delete a branch on the remote, run git push origin --delete BRANCH_NAME. The delete local and remote Git branch guide covers the full flow, including cleanup of stale tracking references.

Why does Git say “branch is not fully merged”?
The branch has commits that are not reachable from your current branch. Git is protecting you from losing work. Review the commits with git log BRANCH_NAME, merge them if needed, and then use -d again. If you are certain the commits are not needed, force the delete with -D.

How do I see which branch a remote branch tracks locally?
Run git branch -vv. The output includes each local branch’s upstream in brackets along with its ahead and behind counts relative to that upstream.

Can I list branches sorted by who committed most recently?
Yes. Use git branch --sort=-committerdate to sort branches by the timestamp of their most recent commit, with the freshest branch first. This is a fast way to see what is actively being worked on.

Conclusion

git branch covers the full lifecycle of a branch: creating it, renaming it, keeping track of what it points at, and deleting it when the work is done. Combined with git switch for moving between branches and git merge for integrating changes, it is the foundation of any Git workflow.

For related Git commands, see the git diff guide for reviewing changes and the git log guide for inspecting branch history.

前端自动化编译Jenkins

前端项目自动化部署 = 3 步

Jenkins 拉取 Git 代码

执行 npm install & npm run build

把 dist 包发布到服务器 /nginx

1.windows安装虚拟机

VMware-workstation-full-17.6.1-24319023.exe link.zhihu.com/?target=htt…

2.下载# Ubuntu镜像

ubuntu.com/download/se…

3.创建虚拟机选择镜像

网络问题使用的最终配置 111网络.png 查看网络:输入 ip a成功标志:看到 ens33 下面有了 inet 192.168.1.x 这样的真实 IP!

如果你之后需要用 Xshell、终端远程连接这台 Ubuntu:保留勾选「Install OpenSSH server」,密码认证也保持勾选即可

111ssh.png

4、SSH 远程连接

在现有 Ubuntu 终端输入:

sudo apt update
sudo apt install openssh-server -y

启动并设置开机自启:

sudo systemctl start ssh
sudo systemctl enable ssh

确认 SSH 运行:

sudo systemctl status ssh

看到 active (running) 就 ok。

确认虚拟机 IP

hostname -I

使用MobaXterm连接虚拟机

5.本地极简安装 Jenkins

直接用 Ubuntu 系统自带包安装 Jenkins(彻底避开 Docker 网络问题)

# 先更新软件源
sudo apt update

# 直接安装OpenJDK(Jenkins依赖)
sudo apt install openjdk-17-jdk -y

# 添加Jenkins官方源密钥
curl -fsSL https://pkg.jenkins.io/debian-stable/jenkins.io-2023.key | sudo gpg --dearmor -o /usr/share/keyrings/jenkins-keyring.gpg

# 添加Jenkins软件源
echo "deb [signed-by=/usr/share/keyrings/jenkins-keyring.gpg] https://pkg.jenkins.io/debian-stable binary/" | sudo tee /etc/apt/sources.list.d/jenkins.list

# 安装Jenkins
sudo apt update
sudo apt install jenkins -y

启动 + 开机自启

sudo systemctl start jenkins
sudo systemctl enable jenkins

检查状态

sudo systemctl status jenkins

✅ 看到 active (running) 就成功!

访问 + 登录

浏览器打开:http://172.17.173.95:8080 获取初始管理员密码:

sudo cat /var/lib/jenkins/secrets/initialAdminPassword

极简兜底方案(国内可用、一步到位)

1. 先清理无效源

sudo rm /etc/apt/sources.list.d/jenkins.list

2. 直接用Jenkins 国内离线包安装,彻底绕开源问题

Jenkins 最新版最低要求 Java 21 / 25,升级系统 Java 到 21

sudo apt install openjdk-21-jdk -y

# 设置默认Java版本
sudo update-alternatives --config java

# 下载Jenkins稳定版war包
wget https://mirrors.tuna.tsinghua.edu.cn/jenkins/war-stable/latest/jenkins.war

# 重新启动现有war包即可
java -jar jenkins.war --httpPort=8080

3.直接启动运行(无需 Docker、无需系统服务)

nohup java -jar jenkins.war --httpPort=8080 > jenkins.log 2>&1 &

6.配置jenkins

1. 必备插件安装

登录 Jenkins → 左侧「系统管理」→「插件管理」→「可选插件」,搜索并安装:

  • Git Plugin(拉取代码)

  • NodeJS Plugin(前端编译环境)

  • 配置 Node.js

    • 找到 NodeJS → 点击 新增NodeJS
    • 自定义名称:node20
    • 勾选 Install automatically
    • 版本选:20.x.x
    • 保存
  • 配置 git

    • 进入 Manage Jenkins(系统管理)
    • 进入 Global Tool Configuration(全局工具配置)
    • 找到 Git
    • 点击 Add Git
    • Name 填:git
    • Path to Git executable 填:/usr/bin/git
    • 保存

2.新建前端构建任务(核心)

新建任务

  • 首页 → 新建任务
  • 名字:前端自动打包
  • 选择:自由风格项目 (Freestyle)
  • 确定

开启「自定义脚本选择」(最关键)

勾选:

This project is parameterized

点击 Add Parameter → Choice Parameter

按下面填写:

  • Name:
RUN_SCRIPT
  • Choices一行一个,对应你 package.json 的 scripts):
build:LNFX
build:LNSY
build:YN
build:EW
build:EA
build:YD
build:GN
build:AHJD
build:GJDT
build:GJDTDB
build:GJDTNM
build:GJDTSX
build:GJDTXA
build:DT
build:DTAH
build:DTJS
build:DTSH
build:JDCC
build:YN2
build:DTXJHM
build:GSZDJ
build:DTHN
build:DTTKT
build:LNDT
build:JXDTJK
build:ZDJGYY
build:ZJJK
build:DTGDZQ
build:GDTWCJK
build:HNYNJK
  • Description:
选择要执行的命令

源码管理(拉代码)

  • Git
  • Repository URL:填你的 Gitee/GitHub 仓库地址
  • Credentials:添加你的仓库账号密码(personal_access_tokens->read_repository权限)
  • Branch:*/main*/master

构建环境

  • 勾选:Provide Node & npm bin/ folder to PATH
  • 选择:node20

Triggers(自动触发 + 定时检测)

  • 只勾选这一项:

    Poll SCM

  • Schedule 填写(直接复制):

 H/2 * * * *

构建步骤(最重要!)

  • 增加构建步骤 → 执行 shell

  • 粘贴下面这段通用前端自动化打包脚本(直接复制):

#!/bin/bash
set -e
# 1. 兼容老项目
export NODE_OPTIONS=--openssl-legacy-provider
# 🔥 强制关闭所有编译检查(关键)
#export DISABLE_ESLINT_PLUGIN=true
#export ESLINT_NO_DEV_ERRORS=true
#export VUE_CLI_SERVICE_NO_ERRORS=true

# 还原正确的 package.json(echarts 4.9.0)
git checkout -- package.json

# 2. 国内镜像
#npm config set registry https://registry.npmmirror.com

# ==========================================
# 🔥 核心:安装时 跳过二进制编译(解决 mozjpeg 报错!)
# 以后加新包也能正常装,同时不炸图片压缩
# ==========================================
install_deps() {
  # --- 1. 仅清理旧的构建产物 ---
  # 这一步必须保留,防止旧代码影响新包
  echo "清理旧的构建产物..."
  rm -rf dist
  rm -rf 70*_dist* # 清理历史文件夹
  rm -f *.zip       # 清理旧压缩包
  
  # 1. 彻底删除可能导致冲突的旧缓存和目录
  #rm -rf node_modules
  #rm -rf package-lock.json
  #rm -rf .cache
  #rm -rf node_modules/.cache
  
  # 2. 清理 npm 全局缓存(防止损坏的包反复被调用)
  #npm cache clean -f
  
  # 3. 重新设置镜像源
  npm config set registry https://registry.npmmirror.com
  
  echo "正在安装依赖..."
  
  # 4. 关键:移除 --ignore-scripts!
  # 如果是因为某个特定包(如 mozjpeg)报错,我们应该针对性解决,而不是全局跳过脚本。
  # 使用 --legacy-peer-deps 处理 Vue2/3 依赖冲突问题
  npm install --legacy-peer-deps

  # 5. 特殊处理 node-sass (如果你的项目还在用它)
  # 很多时候 node-sass 需要手动触发一次 rebuild 才能在 Linux 环境跑通
  if [ -d "node_modules/node-sass" ]; then
    echo "检测到 node-sass,尝试重新构建二进制文件..."
    npm rebuild node-sass
  fi
}

# 每次都执行(新加包能装上)
install_deps

# ====================== 构建逻辑 ======================
# 1. 这里的 DATE 必须和 Node 脚本里的 formattedDate 逻辑完全一致
DATE=$(date +%Y%m%d)
# 2. 这里的项目前缀获取逻辑 (假设你的 RUN_SCRIPT 是 build:LNFX)
# 我们把 build: 后面的名字取出来
PROJECT_NAME=$(echo ${RUN_SCRIPT} | cut -d':' -f2)

# 3. 这里的目录名必须和你的 setup.js 逻辑完全同步:70 + 项目名 + _dist + 日期
DIR_NAME="70${PROJECT_NAME}_dist${DATE}"

echo "====================================="
echo "🚀 预期构建目录: ${DIR_NAME}"
echo "====================================="

echo "====================================="
echo "🚀 开始构建:npm run ${RUN_SCRIPT}"
echo "====================================="

# 在 echo "🚀 预期构建目录: ${DIR_NAME}" 下方加入
if [ -z "${PROJECT_NAME}" ]; then
    echo "❌ 错误:无法从 RUN_SCRIPT (${RUN_SCRIPT}) 中提取项目名称!"
    exit 1
fi

npm run ${RUN_SCRIPT}

# 5. 【关键修复】打包逻辑
# 不再写死 if/else,直接根据上面生成的 DIR_NAME 去找
if [ -d "${DIR_NAME}" ]; then
    echo "✅ 找到目录 ${DIR_NAME},开始压缩..."
    rm -f *.zip
    zip -r "${DIR_NAME}.zip" "${DIR_NAME}"
    echo "✅ 成功生成压缩包: ${DIR_NAME}.zip"
else
    # 容错:打印当前目录下的所有文件夹,方便调试
    echo "❌ 错误:未找到目录 ${DIR_NAME}"
    echo "当前目录下存在的目录有:"
    ls -d */
    exit 1
fi

构建后操作(实现下载)

  • 增加构建后操作 → Archive the artifacts

  • Files to archive 里填:

    plaintext

    70*_dist*.zip
    

7.其他报错

安装zip

sudo apt update && sudo apt install zip -y

图片压缩 依赖的系统库

 sudo apt-get update sudo apt-get install -y autoconf automake libtool libpng-dev libjpeg-dev gcc g++ make

服务器证书验证失败(因为你的 GitLab 是私有地址、自签名证书,Git 默认不让拉)关闭 Git SSL 验证

git config --global http.sslVerify false git config --global https.sslVerify false

Waiting for next available executor

1. 之前的失败任务卡住了“工位”

虽然任务显示失败了,但有时进程可能在后台挂起,占用了执行器。

  • 解决方法

    • 看看左侧栏的 “Build Executor Status” (构建执行状态)。
    • 如果有任务正在运行且进度条不动,点击旁边的 红色小叉叉 强制停止它。

2. 默认并发设置太低

Jenkins 默认可能只设置了 1 个或 2 个执行器。如果你之前的任务还在“取消中”或者有其他任务在排队,它就会显示等待。

  • 手动增加执行器数量

    1. 点击左侧的 Manage Jenkins(管理 Jenkins)。
    2. 点击 Nodes(节点管理,旧版本叫 Manage Nodes and Clouds)。
    3. 点击列表中的 Built-In Node(或者名为 master 的节点)旁边的 齿轮(Configure)
    4. 找到 Number of executors(执行器数量),把它从 1 或 2 改成 5
    5. 点击 Save

重启jenkins

pkill -f jenkins 
pkill -f java
nohup java -jar /home/wzy/jenkins.war --httpPort=8080 > /dev/null 2>&1 &

VMware 扩容

sudo lvextend -l +100%FREE /dev/mapper/ubuntu--vg-ubuntu--lv
sudo resize2fs /dev/mapper/ubuntu--vg-ubuntu--lv

为什么你的 PR 总是多出一堆奇怪的 commit?90% 的人都踩过这个 Git 坑

为什么你的 PR 总是多出一堆奇怪的 commit?90% 的人都踩过这个 Git 坑

在日常开发中,功能分支开发周期较长时,主干分支往往已经有了新的提交。这时需要将主干的最新代码同步到自己的功能分支,常见的方式有两种:mergerebase。本文通过实际场景对比两种方式的区别与使用方法。


背景:为什么需要同步主干?

假设你从主干 main 切出了功能分支 feature/my-work,开发过程中主干有了新的提交,此时你的分支历史如下:

main:    A ── B ── C(新提交)

feature: A ── B ── D(你的提交)

如果不同步主干,提 PR 时可能产生冲突,或者遗漏主干的重要修改。


git pull 是什么?

git pullgit fetch + 合并操作的快捷命令,默认使用 merge 方式:

git pull origin main
# 等价于
git fetch origin
git merge origin/main

也可以指定使用 rebase 方式:

git pull --rebase origin main
# 等价于
git fetch origin
git rebase origin/main

两者的区别与下文 merge / rebase 章节一致。如果希望 git pull 始终使用 rebase,可以设置全局配置:

git config --global pull.rebase true

💡 建议:对于个人功能分支,推荐全局开启 pull.rebase true,保持提交历史线性整洁。


方式一:merge

原理

将主干的最新提交合并进你的分支,并生成一个新的 MergeCommit 记录这次合并。

main:    A ── B ── C
                    \
feature: A ── B ── D ── M(MergeCommit)

操作步骤

第一步:拉取远程最新代码

git fetch origin

第二步:切换到自己的功能分支

git checkout feature/my-work

第三步:合并主干

git merge origin/main

处理冲突(如有)

# 手动解决冲突后
git add .
git merge --continue

# 放弃本次合并
git merge --abort

第四步:推送到远程

git push origin feature/my-work

提 PR 后的 commit 记录

D  你的提交
M  Merge branch 'origin/main' into feature/my-work

方式二:rebase(推荐)

原理

将你的提交「移植」到主干最新提交的后面,历史记录保持线性,不会产生多余的 MergeCommit

main:    A ── B ── C
                    \
feature: A ── B ── C ── D'(你的提交被接在 C 后面)

操作步骤

第一步:拉取远程最新代码

git fetch origin

第二步:切换到自己的功能分支

git checkout feature/my-work

第三步:变基到主干

git rebase origin/main

处理冲突(如有)

rebase 会逐个回放你的每一个 commit,每个 commit 都可能产生冲突,需要逐一处理:

# 手动解决冲突后
git add .
git rebase --continue

# 放弃本次 rebase
git rebase --abort

第四步:force push 到远程

由于 rebase 改写了本地提交历史,必须使用 force push 同步到远程:

git push origin feature/my-work --force-with-lease

⚠️ 为什么用 --force-with-lease 而不是 --force --force 会无条件覆盖远程分支,存在覆盖他人提交的风险。 --force-with-lease 更安全——如果远程分支有你本地没有的新提交,会拒绝推送并提示,避免意外覆盖他人代码。

提 PR 后的 commit 记录

D'  你的提交(仅此一条,干净清晰)

两种方式对比

merge rebase
历史记录 非线性,含 MergeCommit 线性,简洁清晰
PR commit 数量 自己的 commit + MergeCommit 只有自己的 commit
推送方式 正常 git push git push --force-with-lease
冲突处理次数 只处理一次 每个 commit 都可能处理一次
适合场景 多人协作的公共分支 个人功能分支同步主干

常见问题

Q:rebase 时提示 hint: use --reapply-cherry-picks to include skipped commits

这是正常现象。rebase 检测到你分支上的某个 commit 内容已经存在于目标分支(例如之前被 cherry-pick 过),会自动跳过该 commit,避免重复提交。

可以关闭这条提示:

git config --global advice.skippedCherryPicks false

Q:rebase 之后提 PR,为什么还是有重复的 commit?

rebase 只改写了本地历史,如果没有执行 force push,远程分支仍然是旧的历史。PR 提的是远程分支,所以重复的 commit 还是会被带进去。

务必在 rebase 完成后执行:

git push origin feature/my-work --force-with-lease

Q:git pull 默认是 merge,能改成 rebase 吗?

可以,设置全局配置:

# git pull 默认使用 rebase
git config --global pull.rebase true

设置后,git pull 等价于 git pull --rebase,不再产生多余的 MergeCommit。


总结

  • 个人功能分支同步主干,推荐使用 rebase,PR 历史干净,只包含自己真正新增的 commit。
  • 公共分支之间的合并,推荐使用 merge,保留完整历史,不需要 force push,更安全。
  • 无论哪种方式,同步前都应先执行 git fetch origin 获取远程最新状态。

PDF无限制预览!Jit-Viewer V1.5.0开源文档预览神器正式发布

下面和大家分享一下最近我们开源的文档预览SDK——Jit-Viewer,昨天刚发布 1.5.0 版本,和大家分享一下最新的功能更新。

图片

如果你是开发文档预览功能的开发者,一定经历过这种崩溃:txt文档预览乱码、PDF只能看前5页、大文件加载卡顿,代码文件预览毫无章法。

为了帮大家解决这些真实的使用痛点,提升开发体验,我们这段时间优化了 Jit-Viewer 开源文档预览SDK。上周刚帮不少开发者解决了PDF预览受限的问题——终于能完整查看所有PDF文档了。

今天,Jit-Viewer V1.5.0 正式发布,4大核心更新,让文档预览开发更高效、更省心。

文档地址:jitword.com/jit-viewer.…

开源地址:github.com/jitOffice/j…

这次更新,我们重点带来了以下功能:

1. 支持txt多编码格式预览兼容  

图片

之前很多开发者反馈,txt文档预览经常出现乱码,尤其是非UTF-8编码的文件,调试起来特别麻烦,浪费大量时间。

这次更新,我们优化了txt文档解析逻辑,全面兼容ANSI、UTF-8、GBK等多种常见编码格式,不管你导入的txt文件是什么编码,都能正常显示,再也不用手动转换编码、反复调试,帮大家节省更多开发时间。

2. 支持PDF文件完整预览,告别5页限制  

图片

这是本次更新最受期待的功能!之前版本的Jit-Viewer,PDF文件只能预览前5页,对于需要完整预览长文档的开发者来说,实用性大打折扣,很多场景下根本无法满足需求。

图片

这次我们彻底突破了这个限制,底层重构了PDF渲染能力,支持PDF文件全页完整预览,不管是几页的PDF,都能一次性加载完成,搭配原有缩放、翻页功能,完美适配各类PDF预览场景,再也不用为了查看完整PDF额外集成其他工具。

3. 优化SDK预览性能,搭载高性能文件预览引擎  

我们知道,开发者在集成文档预览SDK时,最在意的就是性能——大文件加载慢、切换页面卡顿,都会影响产品体验。这次更新,我们重新设计了文件预览引擎,优化了文件加载、渲染的全流程,大幅提升了预览速度和稳定性,即使是大文档,也能快速加载、流畅切换,不会出现卡顿、崩溃的情况,同时降低了资源占用,让你的应用运行更流畅。

4. 支持代码文件高亮预览  

针对开发类场景,我们新增了代码文件高亮预览功能。不管是Java、Python、JavaScript,还是HTML、CSS等常见编程语言,导入后都能自动识别语言类型,实现语法高亮,代码结构清晰可见,再也不用看着杂乱无章的纯文本代码发愁,尤其适合需要在应用中集成代码预览功能的开发者,大幅提升使用体验。

市面上很多商业文档预览SDK,只解决“能预览”的问题,而 Jit-Viewer 想解决的是“好用、省心、适配多场景”。

这次V1.5.0的更新,本质上是在“轻量高效”的核心定位上,进一步突破场景限制、优化使用体验——让复杂的文档预览开发,变得更简单,让不同需求的开发者,都能快速集成、高效使用,不用再为各类预览问题额外消耗精力。

简单来说,Jit-Viewer 是一个纯前端的文件预览引擎。不需要后端转换服务,不需要安装任何插件,几行代码就能让浏览器具备"专业软件"的预览能力。图片目前 jit-viewer 已经支持了:

  • docx / ppt / pdf / excel
  • csv
  • html
  • markdown
  • txt
  • 代码文件(如js,css, java, go, c#, php, ts等)
  • 音频 / 视频
  • CAD
  • 3D模型
  • OFD(国产格式)

同时我们还在持续迭代优化,帮助大家仅通过几行代码,就能让自己的web系统轻松拥有多种文档预览的能力。

github:github.com/jitOffice/j…

git fetch vs git pull: What Is the Difference?

Sooner or later, every Git user runs into the same question: should you use git fetch or git pull to get the latest changes from the remote? The two commands look similar, and both talk to the remote repository, but they do very different things to your working branch.

This guide explains the difference between git fetch and git pull, how each command works, and when to use one over the other.

Quick Reference

If you want to… Use
Download remote changes without touching your branch git fetch
Download and immediately integrate remote changes git pull
Preview incoming commits before merging git fetch + git log HEAD..origin/main --oneline
Update your branch but refuse merge commits git pull --ff-only
Rebase local commits on top of remote changes git pull --rebase

What git fetch Does

git fetch downloads commits, branches, and tags from a remote repository and stores them locally under remote-tracking references such as origin/main or origin/feature-x. It does not touch your working directory, your current branch, or the index.

Terminal
git fetch origin
output
remote: Enumerating objects: 12, done.
remote: Counting objects: 100% (12/12), done.
From github.com:example/project
4a1c2e3..9f7b8d1 main -> origin/main
a6d4c02..1e2f3a4 feature-x -> origin/feature-x

The output shows which remote branches were updated. After the fetch, the local branch main is still pointing to whatever commit it was on before. Only the remote-tracking branch origin/main has moved.

You can then inspect what changed before doing anything with it:

Terminal
git log main..origin/main

This is the safe way to look at new work without merging or rebasing yet. It gives you a preview of what the remote has that your branch does not.

What git pull Does

git pull is a convenience command. Under the hood, it runs git fetch followed by an integration step (a merge by default, or a rebase if configured). In other words:

Terminal
git pull origin main

is roughly equivalent to:

Terminal
git fetch origin
git merge origin/main

After git pull, your current branch has moved forward, and your working directory may contain new files, updated files, or merge conflicts that need resolving. The change is immediate and affects your checked-out branch.

Side-By-Side Comparison

The difference between the two commands comes down to what they change in your repository.

Aspect git fetch git pull
Downloads new commits from remote Yes Yes
Updates remote-tracking branches (origin/*) Yes Yes
Updates your current local branch No Yes
Modifies the working directory No Yes
Can create merge conflicts No Yes
Safe to run at any time Yes Only when ready to integrate

git fetch is read-only from your branch’s perspective. git pull actively changes your branch.

When to Use git fetch

Use git fetch when you want to see what changed on the remote without integrating anything yet. This is useful before you start new work, before rebasing a feature branch, or when you want to inspect a teammate’s branch safely.

For example, after fetching, you can compare your branch with the remote like this:

Terminal
git log HEAD..origin/main --oneline

This shows commits that exist on the remote but not in your current branch. Running git fetch often is cheap and has no side effects on your work, which is why many developers do it regularly throughout the day.

When to Use git pull

Use git pull when you are ready to bring remote changes into your current branch and keep working on top of them. The common flow looks like this:

Terminal
git checkout main
git pull origin main

This is the quickest way to sync a branch with its upstream when you know the integration will be straightforward, such as on a shared main branch where you rarely have local commits. git pull always updates the branch you currently have checked out, so it is worth confirming where you are before you run it.

If you want a cleaner history without merge commits, configure pull to rebase:

Terminal
git config --global pull.rebase true

From that point on, git pull runs git fetch followed by git rebase instead of git merge. Your local commits are replayed on top of the fetched changes, producing a linear history.

Avoiding Merge Surprises

The biggest reason to prefer git fetch over git pull in day-to-day work is control. A bare git pull on a branch where you have local commits can create a merge commit, introduce conflicts, or rewrite history during a rebase, all in one step.

A safer pattern is:

Terminal
git fetch origin
git log HEAD..origin/main --oneline
git merge origin/main

You fetch first, review what is coming, then decide whether to merge, rebase, or defer the integration. For active feature branches, this workflow avoids most of the “what just happened to my branch” moments.

If you already ran git pull and need to inspect what happened, start with:

Terminal
git log --oneline --decorate -n 5

This helps you confirm whether Git created a merge commit, fast-forwarded the branch, or rebased your local work.

If the pull created a merge commit that you do not want, ORIG_HEAD often points to the commit your branch was on before the pull. In that case, you can reset back to it:

Terminal
git reset --hard ORIG_HEAD
Warning
git reset --hard discards uncommitted changes. Use it only when you are sure you do not need the current state of your working tree.

Useful Flags

A few flags make both commands more predictable in real workflows.

  • git fetch --all - Fetch from every configured remote, not only origin.
  • git fetch --prune - Remove remote-tracking branches that no longer exist on the remote.
  • git fetch --tags - Download tags in addition to branches.
  • git pull --rebase - Rebase your local commits on top of the fetched changes instead of merging.
  • git pull --ff-only - Update the branch only if Git can fast-forward it; prevents unexpected merge commits.

--ff-only is especially useful on shared branches. If your branch cannot move forward by simply advancing the pointer, the pull fails and you decide what to do next.

FAQ

Does git fetch modify any local files?
No. It only updates remote-tracking references such as origin/main. Your branches, working directory, and staging area stay the same.

Is git pull the same as git fetch plus git merge?
By default, yes. With pull.rebase set to true (or --rebase on the command line), it runs git fetch followed by git rebase instead.

Which one should I use daily?
Prefer git fetch for visibility and git pull --ff-only (or git pull --rebase) for the integration step. A bare git pull works, but it hides two operations behind one command.

How do I see what git fetch downloaded?
Use git log main..origin/main to see commits on the remote that are not yet in your local branch, or git diff main origin/main to compare the content directly.

Can git fetch cause conflicts?
No. Conflicts only happen when you merge or rebase the fetched commits into your branch. Fetching itself is conflict-free.

Conclusion

git fetch lets you inspect remote changes without touching your branch, while git pull fetches and integrates those changes in one step. If you want more control and fewer surprises, fetch first, review the incoming commits, and then merge or rebase on your own terms. For more Git workflow details, see the git log command and git diff command guides.

git cherry-pick Command: Apply Commits from Another Branch

Sometimes the change you need is already written, just on the wrong branch. A hotfix may land on main when it also needs to go to a maintenance branch, or a useful commit may be buried in a feature branch that you do not want to merge wholesale. In that situation, git cherry-pick lets you copy the effect of a specific commit onto your current branch.

This guide explains how git cherry-pick works, how to apply one or more commits safely, and how to handle the conflicts that can appear along the way.

Syntax

The general syntax for git cherry-pick is:

txt
git cherry-pick [OPTIONS] COMMIT...
  • OPTIONS - Flags that change how Git applies the commit.
  • COMMIT - One or more commit hashes, branch references, or commit ranges.

git cherry-pick replays the changes introduced by the selected commit on top of your current branch. Git creates a new commit, so the result has a different commit hash even when the file changes are the same.

Cherry-Picking a Single Commit

Start by finding the commit you want to copy. This example lists the recent commits on a feature branch:

Terminal
git log --oneline feature/auth
output
a3f1c92 Fix null pointer in auth handler
d8b22e1 Add login form validation
7c4e003 Refactor session logic

The output gives you the abbreviated commit hashes. In this case, a3f1c92 is the fix we want to move.

Switch to the target branch before running git cherry-pick:

Terminal
git switch main
git cherry-pick a3f1c92
output
[main 9b2d4f1] Fix null pointer in auth handler
Date: Tue Apr 14 10:42:00 2026 +0200
1 file changed, 2 insertions(+)

Git applies the change from a3f1c92 to main and creates a new commit, 9b2d4f1. The subject line is the same, but the commit hash is different because the parent commit is different.

Cherry-Picking Multiple Commits

If you need more than one non-consecutive commit, pass each hash in the order you want Git to apply them:

Terminal
git cherry-pick a3f1c92 d8b22e1

Git creates a separate new commit for each one. This works well when you need a few targeted fixes but do not want the rest of the source branch.

For a range of consecutive commits, use the range notation:

Terminal
git cherry-pick a3f1c92^..7c4e003

This tells Git to include a3f1c92 and every commit after it up to 7c4e003. If you omit the caret, the starting commit itself is excluded:

Terminal
git cherry-pick a3f1c92..7c4e003

That form applies every commit after a3f1c92 through 7c4e003.

Applying Changes Without Committing

Sometimes you want the changes from a commit, but not an automatic commit for each one. Use --no-commit (or -n) to apply the changes to your working tree and staging area without creating the commit yet:

Terminal
git cherry-pick --no-commit a3f1c92

This is useful when you want to combine several small fixes into one commit on the target branch, or when you need to edit the files before committing.

After reviewing the result, create the commit yourself:

Terminal
git status
git commit -m "Backport auth null-check fix"

This gives you more control over the final commit message and lets you group related backports together.

Recording Where the Commit Came From

For maintenance branches and backports, it is often helpful to keep a reference to the original commit. Use -x to append the source commit hash to the new commit message:

Terminal
git cherry-pick -x a3f1c92

Git adds a line like this to the new commit message:

output
(cherry picked from commit a3f1c92...)

That extra line makes future audits easier, especially when you need to prove that a fix on a release branch came from a reviewed change on another branch.

Cherry-Picking a Merge Commit

Cherry-picking a regular commit is straightforward, but merge commits need one extra option. Git must know which parent to treat as the main line:

Terminal
git cherry-pick -m 1 MERGE_COMMIT_HASH
  • -m 1 - Use the first parent as the base, which is usually the branch that received the merge.
  • -m 2 - Use the second parent instead.

If you are not sure which parent is which, inspect the history first:

Terminal
git log --oneline --graph

Cherry-picking merge commits is more advanced and easier to get wrong. If the goal is to bring over an entire merged feature, a normal merge is often clearer than cherry-picking the merge commit itself.

Resolving Conflicts

If the target branch has changed in the same area of code, Git may stop and ask you to resolve a conflict:

output
error: could not apply a3f1c92... Fix null pointer in auth handler
hint: After resolving the conflicts, mark them with
hint: "git add/rm <pathspec>", then run
hint: "git cherry-pick --continue".

Open the conflicted file and look for the conflict markers:

output
<<<<<<< HEAD
return session.getUser();
=======
if (session == null) return null;
return session.getUser();
>>>>>>> a3f1c92 (Fix null pointer in auth handler)

Edit the file to keep the final version you want, then stage it and continue:

Terminal
git add src/auth.js
git cherry-pick --continue

If you decide the commit is not worth applying after all, abort the operation:

Terminal
git cherry-pick --abort

git cherry-pick --abort puts the branch back where it was before the cherry-pick started.

A Safe Backport Workflow

When you cherry-pick onto a release or maintenance branch, slow down and make the source obvious. A simple workflow looks like this:

Terminal
git switch release/1.4
git pull --ff-only
git cherry-pick -x a3f1c92
git status

The important part is the sequence. Start from the branch that needs the fix, make sure it is up to date, cherry-pick with -x, then review and test the branch before pushing it. This avoids the common mistake of copying a fix into an outdated branch and shipping an untested backport.

If the picked commit depends on earlier refactors or new APIs that are not present on the target branch, stop there. In that case, either copy the prerequisite commits too or recreate the fix manually.

When to Use git cherry-pick

git cherry-pick is a good fit when you need a precise change without the rest of the branch:

  • Backporting a bug fix from main to a release branch
  • Recovering one useful commit from an abandoned feature branch
  • Moving a small fix that was committed on the wrong branch
  • Pulling a reviewed change into a hotfix branch without merging unrelated work

Avoid it when the target branch needs the full context of the source branch. If the commit depends on earlier commits, shared refactors, or schema changes, a merge or rebase is usually the cleaner option.

Troubleshooting

error: could not apply ... during cherry-pick
The target branch has conflicting changes. Resolve the files Git marks as conflicted, stage them with git add, then run git cherry-pick --continue.

Cherry-pick created duplicate-looking history
That is normal. Cherry-pick copies the effect of a commit, not the original object. The new commit has a different hash because it has a different parent.

The picked commit does not build on the target branch
The commit likely depends on earlier work that is missing from the target branch. Inspect the source branch history with git log and either cherry-pick the prerequisites too or reimplement the change manually.

You picked the wrong commit
If the cherry-pick already completed, use git revert on the new commit. If the operation is still in progress, use git cherry-pick --abort.

Quick Reference

For a printable quick reference, see the Git cheatsheet .

Task Command
Pick one commit git cherry-pick COMMIT
Pick several specific commits git cherry-pick C1 C2 C3
Pick a consecutive range including the first commit git cherry-pick A^..B
Apply changes without committing git cherry-pick --no-commit COMMIT
Record the source commit in the message git cherry-pick -x COMMIT
Continue after resolving a conflict git cherry-pick --continue
Skip the current commit in a sequence git cherry-pick --skip
Abort the operation git cherry-pick --abort
Pick a merge commit git cherry-pick -m 1 MERGE_COMMIT

FAQ

What is the difference between git cherry-pick and git merge?
git cherry-pick copies the effect of selected commits onto your current branch. git merge joins two branch histories and brings over all commits that are missing from the target branch.

Does cherry-pick change the original commit?
No. The source commit stays exactly where it is. Git creates a new commit on your current branch with the same file changes.

Should I use -x every time?
Not always, but it is a good habit for backports and maintenance branches. It gives you a clear link back to the original commit.

Can I cherry-pick commits from another remote branch?
Yes. Fetch the remote branch first, then cherry-pick the commit hash you want. You can inspect the incoming history with git log or review the file changes with git diff before applying anything.

Conclusion

git cherry-pick is the tool to reach for when one commit matters more than the branch it came from. Use it for targeted fixes, keep -x in mind for backports, and fall back to merge or rebase when the change depends on broader branch context.

数字排毒,我们这代人的赎罪券|硬哲学

很少有 MacBook Neo 评价两极化这么夸张的苹果产品。

有人说它性能完全不够,靠 logo 骗预算不够的小白;有人却说它是近年来苹果最务实的诚意之作。

剥离掉对跑分性能和极致的生产力上限的执念,你会发现,已经很久没有一款科技产品,剔除冗余的性能溢价,也不做不切实际的承诺。你不应该买它,除非你真的需要。

在 MacBook Neo 的身上,有一种真正的「极简主义科技」意味,因为它回应了用户真实需求,也没有人为阉割来制造所谓的极简感。

今天爱范儿这篇专栏文章,想要探讨真正的「极简主义科技」究竟是什么。

我在 Reddit 上看到了很多帖子,描述想象中的极简主义科技生活:主力机是 Light Phone 3 代,这台「笨手机」(dumb phone) 售价 699 美元;一台 iPod Classic 用来听歌;一台十年前的数码相机用来拍照;当有灵感需要记录的时候,他会从自己的「离线包」里掏出手账本……以此类推。

每一个智能手机具备且经常使用的功能,都被分散到一台单独的设备或道具上。日常出街的家伙事,足足三斤重。

帖主给这种状态起了个名字:Inconvenient Maximalism,不方便的极繁主义。

对了,这个「离线包」(analog bag) 也是前段时间洋抖 (TikTok) 上爆红的最新趋势,成千上万的视频,巨大的流量。它指的是一个装满「离线物品」的包:胶片机/CCD、随身听、有线耳机、毛线针和线团、手账本与纸质书……大意是说,当你出门时完全可以把手机扔下,用这包东西打发时间。

我很喜欢的一个「反消费主义者」YouTube 博主 Levi Hildebrand,是这么评价「离线包」的:

手机能做一百件事。所以你不带手机就需要一百样东西来替代它,结果就是你的包包越装越重。

然而更讽刺的是,这些博主明明带着手机,背着包包到处跑,几十个场景机位来回切换,拍出视频……就是为了发到网上,再忽悠他们用自己的返佣链接下单去买这些 CCD、随身听、耳机、毛线针、手账本?

如今的消费主义,已经堕落到这种程度了?为什么这些热衷于「极简主义科技」「数字排毒」的人,如此抽象?

数字排毒,活成了自己的笑话

每当某种注意力收割工具令人厌倦了、过时了,马上就会有新东西,以反抗者、革命者的姿态出现,承诺将你解脱出来……

不消时日,这个新东西就会马上演化成下一轮的收割工具,往复循环。

今天,这个新东西就是「数字排毒」的概念,以及打着这个概念旗号,企图笼络人心的极简主义科技产品们。

2017 年,第一代 Light Phone 上市,只能打电话;2019 年,Light Phone 二代加入了短信、音乐播放器和闹钟。去年,乘上了「笨手机」春风的 Light Phone 三代终于发布,售价 699 美元。

海外媒体是这么评价 Light Phone 三代的:「极简主义被拉伸到令人沮丧的程度」,「一台越来越像智能机的傻瓜手机」。也不能怪他们:AMOLED 屏幕、摄像头、NFC 支付、指纹解锁……只看参数表的话,你很容易以为这就是一台智能机。

从开始到现在,Light Phone 已然进入了两难。如果卖点是「少」,就必须砍功能。但功能少了,用户反而不敢买单;把功能加回来,尺度很微妙。

除了产品设计之外,Light Phone 还面临商业模式的问题。

它最初是从众筹平台上起飞的,但公司随后不得不拿了风投的钱。谈投资的时候可能聊的是「数字排毒」的趋势,投后要看的却是销量、增长、财务……本质上,这套逻辑和极简主义/反消费主义「希望用户少用产品」的美好愿景是完全错位的。

结果就是为了卖货,这台「笨手机」高也不成低也不就,离它最初承诺的东西越来越远,却越来越像它本来要取代的东西……

消费主义的本质,是不断创造新的欲望来消化过剩的产能,而注意力经济是创造消费欲望的最有效手段之一。

哥大法学院的吴修铭教授认为,教授注意力经济也已有百年多的历史。从 19 世纪的廉价报纸 (penny press),到 20 世纪的广播电视,再到今天的短视频、小游戏、短剧,其实注意力经济从来没有变过:用免费内容交换人的时间,再把这些时间通过各种方式(广告、数据等)变现。

哈佛商学院荣休教授苏珊娜·祖博夫在《监控资本主义时代》一书中提出了一个新的概念:「行为剩余」(behavioral surplus),指的是科技公司从用户行为里提取数据,例如你点击了什么、在哪里停留了多久、在哪里犹豫了,然后把它们转化为「行为预测产品」,打包卖给广告商。

但为了让预测更准确,平台需要主动去「塑造」用户行为——无限算法流、消息红点、间歇性的点赞通知,都会服务这个目的。

前几年曾经有个社交产品 BeReal 爆火,每天随机弹出通知,用户必须在两分钟内打开应用拍照并分享,没时间准备,也不给修图滤镜,鼓励展现未修饰的日常样貌,消除社交产品的使用焦虑。

2024 年,以高频成瘾的垃圾手游闻名的法国公司 Voodoo,斥资 5 亿欧元收购了 BeReal。

一款以「反注意力收割」为卖点的产品,反倒被注意力收割机吞并了。这大概就是活成了自己的笑话、屠龙者终成恶龙、逻辑闭环了吧……

这些科技产品的设计理念,本质上和老虎机没什么区别。让人上瘾最有效的方式不是每次都给奖励,而是随机给。你不知道这次下拉会看到什么,正是这个不确定性让你停不下来。

互联网是通讯工具,是知识系统……它可以是很多东西。但大部分时候,它实际上是一台重新引导、无情收割一切注意力的机器。它侵蚀的不只是你的时间,是你的掌握自己注意力的自主性。

买一台「笨手机」,解决不了结构性问题

乔治城大学计算机教授卡尔·纽波特是「数字极简主义」这一概念最重要的推广者。2019 年他出版了《数字极简主义:在喧嚣世界中选择专注的生活》一书,认为电子邮件、聊天、短视频等等助长了「过度活跃的蜂群思维」(the hyperactive hive mind)。

纽波特认为,智能手机应该像一把瑞士军刀,大体上有通话、地图、相机、音乐这些核心功能就够了——这个愿望有点不切实际,他自己也清楚。

于是他转而提倡一种「非暴力」式的戒网方法:在你的手机上关闭非紧急通知,删除社交软件,手机调成单色模式,给自己设定一个数字宵禁的时间。从他的角度,把手机放在家里不带出门,已经是最极端的排毒行为了。

你可以看出来,纽波特的方案本来是零成本的。他从来没说过,你应该花数千块钱购置任何额外的设备。在他的方法论当中,甚至没有「数字排毒产品」这个品类的存在。

然而冥冥之中,提出了数字排毒概念的纽波特,反而成了另一群人,将之商品化的共谋:

  • 首先,有人带着真诚的初衷发现了一个真实问题,触碰到了更多人内心深处某种真实的渴望;
  • 接着,另一群人看到了营销机会,开始兜售一套你必须拥有的东西,来证明你属于这场运动。
  • 结果是,这群人接管、掌握、统治、最终定义了整个运动,直到它的信仰破灭。

类似的剧本一再上演。

1986 年,意大利的第一家麦当劳,在罗马的西班牙广场开业。作家 Carlo Petrini 召集了一群同事朋友去抗议,而这次抗议后来演化成了慢食运动 (Slow Food)。

这场运动的立场,既回归传统又标新立异:反对工业化快餐对饮食的侵蚀,在农民和消费者之间建立更直接的连接。

然而现如今,「农场到餐桌」(farm to table) 早已成了高端食品的标签,慢食运动最初所代表的理念,早已被消费主义完全消化,逐渐退化成了高档餐厅和有机超市溢价的理由。

十多年前,源自于佛教等宗教里的正念/灵修/内观,成为了社交网络上最 in 的潮流之一。然而当这种非主流爱好演变为潮流之后,也成了新的商业收割机。一群科技创业者趁势而上,开发出了市场规模高达数十亿美元的正念产业。

学者 Ronald Purser 在 2019 年出了一本书,书名就叫《麦正念》(McMindfulness,一个很有趣的双关),批评「正念」早已变成让打工人在高压环境里更好适应的减压技术。正念产业们忽略了真正的问题在于结构性的工作压力,却把责任塞还给了个人,让用户去管理自己的内心。

热潮过后,行业两大巨头 Headspace、Calm 的下载量纷纷暴跌(-74%、-61%)。

和「农场到餐桌」「正念」等概念一样,数字排毒也正经历概念破产的加速期。

数字排毒产品许诺的是:用一次性的消费行为,解决一个持续性的行为问题。但如果你看过各种电影电视剧里强制戒酒、戒毒之后的复饮、复吸情节,应该知道这种强硬限制的反效果有多强。

2025 年《BMC Medicine》刊登了一项为期三周的手机使用干预试验,要求上百名参与者每天使用时间不超过两小时。干预期间,测试组的平均屏幕时间从每天 285 分钟降至 129 分钟,压力和睡眠质量也同步改善。

然而 6 周后回访数据显示,他们的屏幕时间又反弹回了 226 分钟,心理健康睡眠指标也降回去了。又过了一周,反弹情况已经和测试的对照组基线水平无异。

强制、短期的「戒网」,是没有效果的。

为什么这种限制注定会失败?上世纪 60 年代,心理学家 Jack Brehm 提出「心理抗拒理论」:当一个人感知到自己的自由选择被外部力量限制时,他会产生强烈的动机来恢复这种自由。

约束越强硬,被禁止的行为就越有吸引力。这解释了为什么很多「笨手机」用户最终把那台手机放进了抽屉,然后换回了 iPhone 和安卓机。

数字排毒产品赖以存在的心理前提,可能本身就是错的。

用消费,反抗消费

回到文章开头提到的「离线包」案例。

如果你去刷相关话题的视频,会发现这些博主,各种书哐哐往家买,却没几个真的去博物馆借书的;这群人的视频里一定有个漂漂亮亮的手账本,里面到底记了啥你是不太可能看到的。

用户在博主们的影响下,花了成百上千块钱,买了各种各样的产品。然而,却形成了「意识到问题了」「在行动了」,就等于题被解决了的错觉。

这种行为其实很像中世纪西方大受欢迎的「赎罪券」。信徒不用改变自己的行为,甚至不需要告解和悔改,只需要支付金钱就能获得「罪已得赦」的心理确认。钱花出去,药到病除,非常方便。

⬆17 世纪的赎罪券(后世复刻)

这种现象在行为经济学上叫做「道德许可效应」。当人们完成了一件感觉道德的事情之后,更容易在其他方面放纵自己。比如购买 Patagonia 等环保品牌,很容易让你误以为自己真为地球环保做了多大贡献——其实从个体角度,你的不仅助长了消费主义,甚至制造了更多的垃圾。

我们为什么执迷于用消费行为来反抗消费主义,却以为自己还挺聪明?其实,这里面有人格方面的深层原因。

人是注意力动物。无论是手机,还是离线包里装的各种各样的东西,其存在目的都是牵制住人的注意力,帮我们杀掉时间。如果没有这些东西占据注意力,我们会变得极度无聊。

究其根本,在人人都有智能手机、人人都随时在线的时代,我们已经不知道一个人不刷手机该怎么呆着了。如果注意力充裕却没有地方可放,我们甚至会恐慌。所以我们需要一种「干扰」。

而这些数字排毒产品,其实是在利用你的不安全感,来赚你的钱。他们真的解决了什么痛点吗?恐怕没有。

不只是数字排毒产品,所有的消费陷阱,都是同一套底层逻辑。

如果你要用消费行为来反抗消费主义,恐怕只有资本最后成为赢家。

真正有用的方法,免费但无聊

2024 年,斯坦福大学社交媒体实验室让 80 多名学生用「笨手机」替代智能手机使用一周,初步发现受试者的头脑更清晰,更专注,更活在当下。

但进一步调研发现,主要归因并不是「笨手机」,而是那些报名参加了这项研究的学生,原本就有「数字排毒」的动机。

其实真正有效的方法是完全免费的,只是有点无聊。

豆瓣上有个「数字极简主义者」小组,有 3.3 万成员,我们分析总结了里面的很多帖子,发现最主流和有效的方法,就是少用、不用手机。

比如和家人约定晚饭时间不看手机,比如睡前把手机放到卧室,用真的闹钟来定闹铃;非要用的话,可以巧用,比如把社媒 App 移到手机的第三屏,增加「找到它」的摩擦成本;使用的时候大量点击与兴趣无关的内容,故意污染平台「千人千面」的推荐算法。

是的,实现数字排毒,不需要购买任何新的产品,你唯一需要支付的成本,不应该是金钱,而是心智。但你可以一点一点来,比如先从躺到床上就不再用手机开始。

究其根本,你要承受戒断反应,学会与无聊做朋友。

在注意力高度碎片化的今天,其实「无聊」是件很好的事。如果你能领悟无聊的意义,开始享受无聊,进而换种方式利用无聊的时间,「数字排毒」才能真正开始。

无论如何,都不要再买赎罪券了,那都是骗子发明出来骗傻子的。

#欢迎关注爱范儿官方微信公众号:爱范儿(微信号:ifanr),更多精彩内容第一时间为您奉上。

Harbeth:高性能Metal图像处理库,让你的图片处理速度飞起来!

🎯 项目简介

Harbeth是一个基于Metal的高性能图像处理库,为iOS和macOS开发者提供了一套简洁、高效的图像滤镜解决方案。它不仅支持传统的图像滤镜效果,还能处理HDR图像,让你的应用在图像处理方面如虎添翼。

用故障艺术美学建立动态RGB通道分离 实时检测边缘并添加霓虹灯发光效果
ShiftGlitch.gif EdgeGlow.gif

✨ 核心功能与优势

1. 高性能Metal渲染

  • 利用Metal GPU加速,处理速度比CPU实现快10-50倍
  • 支持命令缓冲区池管理,优化GPU资源使用
  • 双缓冲技术,进一步提升处理性能

2. 丰富的滤镜效果

  • 基础滤镜:亮度、对比度、饱和度、色相调整
  • 高级效果:高斯模糊、锐化、边缘检测、色调映射
  • 创意滤镜:复古、赛博朋克、电影效果、HDR增强
  • 自定义滤镜:支持自定义Metal着色器

超过 200+ 内置滤镜,组织成直观的类别,涵盖从基本颜色调整到高级艺术效果的各种功能

3. HDR图像处理

  • 支持rgba16Float和rgba32Float格式的HDR纹理
  • 内置HDR到SDR的色调映射算法
  • 保留HDR图像的细节和动态范围

4. 易用的API设计

  • 链式调用风格,代码简洁易读
  • 统一的输入输出接口,支持多种图像类型
  • 异步处理支持,避免主线程阻塞

5. 跨平台支持

  • 同时支持iOS、macOS和tvOS
  • 适配不同设备的Metal性能特性
  • 自动处理设备内存限制

🚀 快速开始

安装方式

// Swift Package Manager
.package(url: "https://github.com/yangKJ/Harbeth.git", from: "0.0.1")

// CocoaPods
pod 'Harbeth'

基础使用示例

import Harbeth

// 加载图像
let image = UIImage(named: "example")!

// 创建滤镜
let filter = C7Brightness(brightness: 0.2)

// 应用滤镜
let result = try? HarbethIO(element: image, filter: filter).output() as? UIImage

// 显示结果
imageView.image = result

链式滤镜示例

// 组合多个滤镜
let filters: [C7FilterProtocol] = [
    C7Brightness(brightness: 0.1),
    C7Contrast(contrast: 1.2),
    C7Saturation(saturation: 1.3),
    C7GaussianBlur(radius: 2.0)
]

// 应用滤镜链
let result = try? HarbethIO(element: image, filters: filters).output() as? UIImage

🎨 高级特性

1. 自定义Metal着色器

// 创建自定义滤镜
struct CustomFilter: C7FilterProtocol {
    var modifier: ModifierEnum {
        return .compute(kernel: "customKernel")
    }
    
    var factors: [Float] = [0.5, 0.5, 0.5]
}

// 应用自定义滤镜
let customFilter = CustomFilter()
let result = try? HarbethIO(element: image, filter: customFilter).output() as? UIImage

2. HDR图像处理

// 加载HDR图像
let hdrImage = UIImage(named: "hdr_example")!

// 应用HDR到SDR转换
let hdrFilter = HDRToSDR()
let result = try? HarbethIO(element: hdrImage, filter: hdrFilter).output() as? UIImage

3. 实时处理

// 实时处理相机捕获的图像
func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
    // 异步处理
    HarbethIO(element: sampleBuffer, filter: C7Vibrance(vibrance: 0.5)).transmitOutput { result in
        switch result {
        case .success(let processedBuffer):
            // 处理成功,显示结果
            DispatchQueue.main.async {
                self.previewLayer.enqueue(processedBuffer)
            }
        case .failure(let error):
            print("处理失败: \(error)")
        }
    }
}

⚡ 性能优势

Harbeth在性能方面的表现令人印象深刻:

  • 处理速度:比Core Image快3-5倍,比CPU处理快10-50倍
  • 内存使用:智能纹理池管理,减少内存分配
  • 电池消耗:优化的GPU使用,降低能耗
  • 大图像处理:支持处理高分辨率图像和视频帧

📱 适用场景

Harbeth适用于各种需要图像处理的场景:

  1. 照片编辑应用:快速应用滤镜效果
  2. 相机应用:实时预览和处理
  3. 视频编辑:逐帧处理视频
  4. AR/VR应用:实时图像处理
  5. 社交媒体:快速滤镜效果
  6. HDR图像处理:专业图像处理

🖥️ macOS 支持

Harbeth 完全支持 macOS 平台,为桌面应用提供强大的图像处理能力,打造原生、优化的用户体验:

🎨 macOS 展示

探索 Harbeth 在 macOS 上的强大功能:

🌟 总结

Harbeth是一个功能强大、性能优异的Metal图像处理库,它不仅提供了丰富的滤镜效果,还支持HDR图像处理,为iOS和macOS开发者提供了一套完整的图像处理解决方案。

无论是快速原型开发还是生产环境应用,Harbeth都能满足你的需求。它的高性能特性让图像处理不再成为应用的性能瓶颈,而简洁的API设计则让开发过程更加愉快。

如果你正在寻找一个强大而灵活的图像处理库,Harbeth绝对值得尝试!

📁 项目链接

  • GitHub: github.com/yangKJ/Harb…
  • 文档: 详细的API文档和使用示例
  • 示例应用: 包含多种使用场景的示例代码

让Harbeth为你的应用添加绚丽的图像处理能力,让每一张图片都成为艺术品! 🎨✨

git clone: Clone a Repository

git clone copies an existing Git repository into a new directory on your local machine. It sets up tracking branches for each remote branch and creates a remote called origin pointing back to the source.

This guide explains how to use git clone with the most common options and protocols.

Syntax

The basic syntax of the git clone command is:

txt
git clone [OPTIONS] REPOSITORY [DIRECTORY]

REPOSITORY is the URL or path of the repository to clone. DIRECTORY is optional; if omitted, Git uses the repository name as the directory name.

Clone a Remote Repository

The most common way to clone a repository is over HTTPS. For example, to clone a GitHub repository:

Terminal
git clone https://github.com/user/repo.git

This creates a repo directory in the current working directory, initializes a .git directory inside it, and checks out the default branch.

You can also clone over SSH if you have an SSH key configured :

Terminal
git clone git@github.com:user/repo.git

SSH is generally preferred for repositories you will push to, as it does not require entering credentials each time.

Clone into a Specific Directory

By default, git clone creates a directory named after the repository. To clone into a different directory, pass the target path as the second argument:

Terminal
git clone https://github.com/user/repo.git my-project

To clone into the current directory, use . as the target:

Terminal
git clone https://github.com/user/repo.git .
Warning
Cloning into . requires the current directory to be empty.

Clone a Specific Branch

By default, git clone checks out the default branch (usually main or master). To clone and check out a different branch, use the -b option:

Terminal
git clone -b develop https://github.com/user/repo.git

The full repository history is still downloaded; only the checked-out branch differs. To limit the download to a single branch, combine -b with --single-branch:

Terminal
git clone -b develop --single-branch https://github.com/user/repo.git

Shallow Clone

A shallow clone downloads only a limited number of recent commits rather than the full history. This is useful for speeding up the clone of large repositories when you do not need the full commit history.

To clone with only the latest commit:

Terminal
git clone --depth 1 https://github.com/user/repo.git

To include the last 10 commits:

Terminal
git clone --depth 10 https://github.com/user/repo.git

Shallow clones are commonly used in CI/CD pipelines to reduce download time. To convert a shallow clone to a full clone later, run:

Terminal
git fetch --unshallow

Clone a Local Repository

git clone also works with local paths. This is useful for creating an isolated copy for testing:

Terminal
git clone /path/to/local/repo new-copy

Troubleshooting

Destination directory is not empty
git clone URL . works only when the current directory is empty. If files already exist, clone into a new directory or move the existing files out of the way first.

Authentication failed over HTTPS
Most Git hosting services no longer accept account passwords for Git operations over HTTPS. Use a personal access token or configure a credential manager so Git can authenticate securely.

Permission denied (publickey)
This error usually means your SSH key is missing, not loaded into your SSH agent, or not added to your account on the Git hosting service. Verify your SSH setup before retrying the clone.

Host key verification failed
SSH could not verify the identity of the remote server. Confirm you are connecting to the correct host, then accept or update the host key in ~/.ssh/known_hosts.

Quick Reference

For a printable quick reference, see the Git cheatsheet .

Command Description
git clone URL Clone a repository via HTTPS or SSH
git clone URL dir Clone into a specific directory
git clone URL . Clone into the current (empty) directory
git clone -b branch URL Clone and check out a specific branch
git clone -b branch --single-branch URL Clone only a specific branch
git clone --depth 1 URL Shallow clone: latest commit only
git clone --depth N URL Shallow clone: last N commits
git clone /path/to/repo Clone a local repository

FAQ

What is the difference between HTTPS and SSH cloning?
HTTPS cloning works out of the box but prompts for credentials unless you use a credential manager. SSH cloning requires a key pair to be configured but does not prompt for a password on each push or pull.

Does git clone download all branches?
Yes. All remote branches are fetched, but only the default branch is checked out locally. Use git branch -r to list all remote branches, or git checkout branch-name to switch to one.

How do I clone a private repository?
For HTTPS, provide your username and a personal access token when prompted. For SSH, ensure your public key is added to your account on the hosting service.

Can I clone a specific tag instead of a branch?
Yes. Use -b with the tag name: git clone -b v1.0.0 https://github.com/user/repo.git. Git checks out that tag in a detached HEAD state, so create a branch if you plan to make commits.

Conclusion

git clone is the starting point for working with any existing Git repository. After cloning, you can inspect the commit history with git log , review changes with git diff , learn when to use git fetch vs git pull , and configure your username and email if you have not done so already.

git diff Command: Show Changes Between Commits

Every change in a Git repository can be inspected before it is staged or committed. The git diff command shows the exact lines that were added, removed, or modified — between your working directory, the staging area, and any two commits or branches.

This guide explains how to use git diff to review changes at every stage of your workflow.

Basic Usage

Run git diff with no arguments to see all unstaged changes — differences between your working directory and the staging area:

Terminal
git diff
output
diff --git a/app.py b/app.py
index 3b1a2c4..7d8e9f0 100644
--- a/app.py
+++ b/app.py
@@ -10,6 +10,7 @@ def main():
print("Starting app")
+ print("Debug mode enabled")
run()

Lines starting with + were added. Lines starting with - were removed. Lines with no prefix are context — unchanged lines shown for reference.

If nothing is shown, all changes are already staged or there are no changes at all.

Staged Changes

To see changes that are staged and ready to commit, use --staged (or its synonym --cached):

Terminal
git diff --staged

This compares the staging area against the last commit. It shows exactly what will go into the next commit.

Terminal
git diff --cached

Both flags are equivalent. Use whichever you prefer.

Comparing Commits

Pass two commit hashes to compare them directly:

Terminal
git diff abc1234 def5678

To compare a commit against its parent, use the ^ suffix:

Terminal
git diff HEAD^ HEAD

HEAD refers to the latest commit. HEAD^ is one commit before it. You can go further back with HEAD~2, HEAD~3, and so on.

To see what changed in the last commit:

Terminal
git diff HEAD^ HEAD

Comparing Branches

Use the same syntax to compare two branches:

Terminal
git diff main feature-branch

This shows all differences between the tips of the two branches.

To see only what a branch adds relative to main — excluding changes already in main — use the three-dot notation:

Terminal
git diff main...feature-branch

The three-dot form finds the common ancestor of both branches and diffs from there to the tip of feature-branch. This is the most useful form for reviewing a pull request before merging.

Comparing a Specific File

To limit the diff to a single file, pass the path after --:

Terminal
git diff -- path/to/file.py

The -- separator tells Git the argument is a file path, not a branch name. Combine it with commit references to compare a file across commits:

Terminal
git diff HEAD~3 HEAD -- path/to/file.py

Summary with –stat

To see a summary of which files changed and how many lines were added or removed — without the full diff — use --stat:

Terminal
git diff --stat
output
 app.py | 3 +++
config.yml | 1 -
2 files changed, 3 insertions(+), 1 deletion(-)

This is useful for a quick overview before reviewing the full diff.

Word-Level Diff

By default, git diff highlights changed lines. To highlight only the changed words within a line, use --word-diff:

Terminal
git diff --word-diff
output
@@ -10,6 +10,6 @@ def main():
print("[-Starting-]{+Running+} app")

Removed words appear in [-brackets-] and added words in {+braces+}. This is especially useful for prose or configuration files where only part of a line changes.

Ignoring Whitespace

To ignore whitespace-only changes, use -w:

Terminal
git diff -w

Use -b to ignore changes in the amount of whitespace (but not all whitespace):

Terminal
git diff -b

These options are useful when reviewing files that have been reformatted or indented differently.

Quick Reference

For a printable quick reference, see the Git cheatsheet .

Command Description
git diff Unstaged changes in working directory
git diff --staged Staged changes ready to commit
git diff HEAD^ HEAD Changes in the last commit
git diff abc1234 def5678 Diff between two commits
git diff main feature Diff between two branches
git diff main...feature Changes in feature since branching from main
git diff -- file Diff for a specific file
git diff --stat Summary of changed files and line counts
git diff --word-diff Word-level diff
git diff -w Ignore all whitespace changes

FAQ

What is the difference between git diff and git diff --staged?
git diff shows changes in your working directory that have not been staged yet. git diff --staged shows changes that have been staged with git add and are ready to commit. To see all changes — staged and unstaged combined — use git diff HEAD.

What does the three-dot ... notation do in git diff?
git diff main...feature finds the common ancestor of main and feature and shows the diff from that ancestor to the tip of feature. This isolates the changes that the feature branch introduces, ignoring any new commits in main since the branch was created. In git diff, the two-dot form is just another way to name the two revision endpoints, so git diff main..feature is effectively the same as git diff main feature.

How do I see only the file names that changed, not the full diff?
Use git diff --name-only. To include the change status (modified, added, deleted), use git diff --name-status instead.

How do I compare my local branch against the remote?
Fetch first to update remote-tracking refs, then diff against the updated remote branch. To see what your current branch changes relative to origin/main, use git fetch && git diff origin/main...HEAD. If you want a direct endpoint comparison instead, use git fetch && git diff origin/main HEAD. If you want a clearer overview of what git fetch changes compared with git pull, see our guide on git fetch vs git pull .

Can I use git diff to generate a patch file?
Yes. Redirect the output to a file: git diff > changes.patch. Apply it on another machine with git apply changes.patch.

Conclusion

git diff is the primary tool for reviewing changes before you stage or commit them. Use git diff for unstaged work, git diff --staged before committing, and git diff main...feature to review a branch before merging. For a history of committed changes, see the git log guide , and for syncing remote changes safely, see git fetch vs git pull .

git log Command: View Commit History

Every commit in a Git repository is recorded in the project history. The git log command lets you browse that history — showing who made each change, when, and why. It is one of the most frequently used Git commands and one of the most flexible.

This guide explains how to use git log to view, filter, and format commit history.

Basic Usage

Run git log with no arguments to see the full commit history of the current branch, starting from the most recent commit:

Terminal
git log
output
commit a3f1d2e4b5c6789012345678901234567890abcd
Author: Jane Smith <jane@example.com>
Date: Mon Mar 10 14:22:31 2026 +0100
Add user authentication module
commit 7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c
Author: John Doe <john@example.com>
Date: Fri Mar 7 09:15:04 2026 +0100
Fix login form validation

Each entry shows the full commit hash, author name and email, date, and commit message. Press q to exit the log view.

Compact Output with –oneline

The default output is verbose. Use --oneline to show one commit per line — the abbreviated hash and the subject line only:

Terminal
git log --oneline
output
a3f1d2e Add user authentication module
7b8c9d0 Fix login form validation
c1d2e3f Update README with setup instructions

This is useful for a quick overview of recent work.

Limiting the Number of Commits

To show only the last N commits, pass -n followed by the number:

Terminal
git log -n 5

You can also write it without the space:

Terminal
git log -5

Filtering by Author

Use --author to show only commits by a specific person. The value is matched as a substring against the author name or email:

Terminal
git log --author="Jane"

To match multiple authors, pass --author more than once:

Terminal
git log --author="Jane" --author="John"

Filtering by Date

Use --since and --until to limit commits to a date range. These options accept a variety of formats:

Terminal
git log --since="2 weeks ago"
git log --since="2026-03-01" --until="2026-03-15"

Searching Commit Messages

Use --grep to find commits whose message contains a keyword:

Terminal
git log --grep="authentication"

The search is case-sensitive by default. Add -i to make it case-insensitive:

Terminal
git log -i --grep="fix"

Viewing Changed Files

To see a summary of which files were changed in each commit without the full diff, use --stat:

Terminal
git log --stat
output
commit a3f1d2e4b5c6789012345678901234567890abcd
Author: Jane Smith <jane@example.com>
Date: Mon Mar 10 14:22:31 2026 +0100
Add user authentication module
src/auth.py | 84 ++++++++++++++++++++++++++++++++++++++++++++++++
src/models.py | 12 +++++++
tests/test_auth.py | 45 +++++++++++++++++
3 files changed, 141 insertions(+)

To see the full diff for each commit, use -p (or --patch):

Terminal
git log -p

Combine with -n to limit the output:

Terminal
git log -p -3

Viewing History of a Specific File

To see only the commits that touched a particular file, pass the file path after --:

Terminal
git log -- path/to/file.py

The -- separator tells Git that what follows is a path, not a branch name. Add -p to see the exact changes made to that file in each commit:

Terminal
git log -p -- path/to/file.py

Branch and Graph View

To visualize how branches diverge and merge, use --graph:

Terminal
git log --oneline --graph
output
* a3f1d2e Add user authentication module
* 7b8c9d0 Fix login form validation
| * 3e4f5a6 WIP: dark mode toggle
|/
* c1d2e3f Update README with setup instructions

Add --all to include all branches, not just the current one:

Terminal
git log --oneline --graph --all

This gives a full picture of the repository’s branch and merge history.

Comparing Two Branches

To see commits that are in one branch but not another, use the .. notation:

Terminal
git log main..feature-branch

This shows commits reachable from feature-branch that are not yet in main — useful for reviewing what a branch adds before merging.

Custom Output Format

Use --pretty=format: to define exactly what each log line shows. Some useful placeholders:

  • %h — abbreviated commit hash
  • %an — author name
  • %ar — author date, relative
  • %s — subject (first line of commit message)

For example:

Terminal
git log --pretty=format:"%h %an %ar %s"
output
a3f1d2e Jane Smith 5 days ago Add user authentication module
7b8c9d0 John Doe 8 days ago Fix login form validation

Excluding Merge Commits

To hide merge commits and see only substantive changes, use --no-merges:

Terminal
git log --no-merges --oneline

Quick Reference

For a printable quick reference, see the Git cheatsheet .

Command Description
git log Full commit history
git log --oneline One line per commit
git log -n 10 Last 10 commits
git log --author="Name" Filter by author
git log --since="2 weeks ago" Filter by date
git log --grep="keyword" Search commit messages
git log --stat Show changed files per commit
git log -p Show full diff per commit
git log -- file History of a specific file
git log --oneline --graph Visual branch graph
git log --oneline --graph --all Graph of all branches
git log main..feature Commits in feature not in main
git log --no-merges Exclude merge commits
git log --pretty=format:"%h %an %s" Custom output format

FAQ

How do I search for a commit that changed a specific piece of code?
Use -S (the “pickaxe” option) to find commits that added or removed a specific string: git log -S "function_name". For regex patterns, use -G "pattern" instead.

How do I see who last changed each line of a file?
Use git blame path/to/file. It annotates every line with the commit hash, author, and date of the last change.

What is the difference between git log and git reflog?
git log shows the commit history of the current branch. git reflog shows every movement of HEAD — including commits that were reset, rebased, or are no longer reachable from any branch. It is the main recovery tool if you lose commits accidentally.

How do I find a commit by its message?
Use git log --grep="search term". For case-insensitive search add -i. If you need to search the full commit message body as well as the subject, add --all-match only when combining multiple --grep patterns, or use git log --grep="search term" --format=full to inspect matching commits more closely. Use -E only when you need extended regular expressions in the grep pattern.

How do I show a single commit in full detail?
Use git show <hash>. It prints the commit metadata and the full diff. To show just the files changed without the diff, use git show --stat <hash>.

Conclusion

git log is the primary tool for understanding a project’s history. Start with --oneline for a quick overview, use --graph --all to visualize branches, and combine --author, --since, and --grep to zero in on specific changes. For undoing commits found in the log, see the git revert guide or how to undo the last commit . If you need to move one specific change onto another branch, see git cherry-pick .

git stash: Save and Restore Uncommitted Changes

When you are in the middle of a feature and need to switch branches to fix a bug or review someone else’s work, you have a problem: your changes are not ready to commit, but you cannot switch branches with a dirty working tree. git stash solves this by saving your uncommitted changes to a temporary stack so you can restore them later.

This guide explains how to use git stash to save, list, apply, and delete stashed changes.

Basic Usage

The simplest form saves all modifications to tracked files and reverts the working tree to match the last commit:

Terminal
git stash
output
Saved working directory and index state WIP on main: abc1234 Add login page

Your working tree is now clean. You can switch branches, pull updates, or apply a hotfix. When you are ready to return to your work, restore the stash.

By default, git stash saves both staged and unstaged changes to tracked files. It does not save untracked or ignored files unless you explicitly include them (see below).

Stashing with a Message

A plain git stash entry shows the branch and last commit message, which becomes hard to read when you have multiple stashes. Use -m to attach a descriptive label:

Terminal
git stash push -m "WIP: user authentication form"

This makes it easy to identify the right stash when you list them later.

Listing Stashes

To see all saved stashes, run:

Terminal
git stash list
output
stash@{0}: On main: WIP: user authentication form
stash@{1}: WIP on main: abc1234 Add login page

Stashes are indexed from newest (stash@{0}) to oldest. The index is used when you want to apply or drop a specific stash.

To inspect the contents of a stash before applying it, use git stash show:

Terminal
git stash show stash@{0}

Add -p to see the full diff:

Terminal
git stash show -p stash@{0}

Applying Stashes

There are two ways to restore a stash.

git stash pop applies the most recent stash and removes it from the stash list:

Terminal
git stash pop

git stash apply applies a stash but keeps it in the list so you can apply it again or to another branch:

Terminal
git stash apply

To apply a specific stash by index, pass the stash reference:

Terminal
git stash apply stash@{1}

If applying the stash causes conflicts, resolve them the same way you would resolve a merge conflict, then stage the resolved files.

Stashing Untracked and Ignored Files

By default, git stash only saves changes to files that Git already tracks. New files you have not yet staged are left behind.

Use -u (or --include-untracked) to include untracked files:

Terminal
git stash -u

To include both untracked and ignored files — for example, generated build artifacts or local config files — use -a (or --all):

Terminal
git stash -a

Partial Stash

If you only want to stash some of your changes and leave others in the working tree, use the -p (or --patch) flag to interactively select which hunks to stash:

Terminal
git stash -p

Git will step through each changed hunk and ask whether to stash it. Type y to stash the hunk, n to leave it, or ? for a full list of options.

Creating a Branch from a Stash

If you have been working on a stash for a while and the branch has diverged enough to cause conflicts on apply, you can create a new branch from the stash directly:

Terminal
git stash branch new-branch-name

This creates a new branch, checks it out at the commit where the stash was originally made, applies the stash, and drops it from the list if it applied cleanly. It is the safest way to resume stashed work that has grown out of sync with the base branch.

Deleting Stashes

To remove a specific stash once you no longer need it:

Terminal
git stash drop stash@{0}

To delete all stashes at once:

Terminal
git stash clear

Use clear with care — there is no undo.

Quick Reference

For a printable quick reference, see the Git cheatsheet .

Command Description
git stash Stash staged and unstaged changes
git stash push -m "message" Stash with a descriptive label
git stash -u Include untracked files
git stash -a Include untracked and ignored files
git stash -p Interactively choose what to stash
git stash list List all stashes
git stash show stash@{0} Show changed files in a stash
git stash show -p stash@{0} Show full diff of a stash
git stash pop Apply and remove the latest stash
git stash apply stash@{0} Apply a stash without removing it
git stash branch branch-name Create a branch from the latest stash
git stash drop stash@{0} Delete a specific stash
git stash clear Delete all stashes

FAQ

What is the difference between git stash pop and git stash apply?
pop applies the stash and removes it from the stash list. apply restores the changes but keeps the stash entry so you can apply it again — for example, to multiple branches. Use pop for normal day-to-day use and apply when you need to reuse the stash.

Does git stash save untracked files?
No, not by default. Run git stash -u to include untracked files, or git stash -a to also include files that match your .gitignore patterns.

Can I stash only specific files?
Yes. In modern Git, you can pass pathspecs to git stash push, for example: git stash push -m "label" -- path/to/file. If you need a more portable workflow, use git stash -p to interactively select which hunks to stash.

How do I recover a dropped stash?
If you accidentally ran git stash drop or git stash clear, the underlying commit may still exist for a while. Run git fsck --no-reflogs | grep "dangling commit" to list unreachable commits, then git show <hash> to inspect them. If you find the right stash commit, you can recreate it with git stash store -m "recovered stash" <hash> and then apply it normally.

Can I apply a stash to a different branch?
Yes. Switch to the target branch with git switch branch-name, then run git stash apply stash@{0}. If there are no conflicts, the changes are applied cleanly. If the branches have diverged significantly, consider git stash branch to create a fresh branch from the stash instead.

Conclusion

git stash is the cleanest way to set aside incomplete work without creating a throwaway commit. Use git stash push -m to keep your stash list readable, prefer pop for one-time restores, and use git stash branch when applying becomes messy. For a broader overview of Git workflows, see the Git cheatsheet or the git revert guide .

How to Install Git on Debian 13

Git is the world’s most popular distributed version control system used by many open-source and commercial projects. It allows you to collaborate on projects with fellow developers, keep track of your code changes, revert to previous stages, create branches , and more.

This guide covers installing and configuring Git on Debian 13 (Trixie) using apt or by compiling from source.

Quick Reference

For a printable quick reference, see the Git cheatsheet .

Task Command
Install Git (apt) sudo apt install git
Check Git version git --version
Set username git config --global user.name "Your Name"
Set email git config --global user.email "you@example.com"
View config git config --list

Installing Git with Apt

This is the quickest way to install Git on Debian.

Check if Git is already installed:

Terminal
git --version

If Git is not installed, you will see a “command not found” message. Otherwise, it shows the installed version.

Use the apt package manager to install Git:

Terminal
sudo apt update
sudo apt install git

Verify the installation:

Terminal
git --version

Debian 13 stable currently provides Git 2.47.3:

output
git version 2.47.3

You can now start configuring Git.

When a new version of Git is released, you can update using sudo apt update && sudo apt upgrade.

Installing Git from Source

The main benefit of installing Git from source is that you can compile any version you want. However, you cannot maintain your installation through the apt package manager.

Install the build dependencies:

Terminal
sudo apt update
sudo apt install libcurl4-gnutls-dev libexpat1-dev cmake gettext libz-dev libssl-dev gcc wget

Visit the Git download page to find the latest version.

At the time of writing, the latest stable Git version is 2.53.0.

If you need a different version, visit the Git archive to find available releases.

Download and extract the source to /usr/src:

Terminal
wget -c https://mirrors.edge.kernel.org/pub/software/scm/git/git-2.53.0.tar.gz -O - | sudo tar -xz -C /usr/src

Navigate to the source directory and compile:

Terminal
cd /usr/src/git-*
sudo make prefix=/usr/local all
sudo make prefix=/usr/local install

The compilation may take some time depending on your system.

If your shell still resolves /usr/bin/git after installation, open a new terminal or verify your PATH and binary location with:

Terminal
which git
echo $PATH

Verify the installation:

Terminal
git --version
output
git version 2.53.0

To upgrade to a newer version later, repeat the same process with the new version number.

Configuring Git

After installing Git, configure your username and email address. Git associates your identity with every commit you make.

Set your global commit name and email:

Terminal
git config --global user.name "Your Name"
git config --global user.email "youremail@yourdomain.com"

Verify the configuration:

Terminal
git config --list
output
user.name=Your Name
user.email=youremail@yourdomain.com

The configuration is stored in ~/.gitconfig:

~/.gitconfigconf
[user]
name = Your Name
email = youremail@yourdomain.com

You can edit the configuration using the git config command or by editing ~/.gitconfig directly.

For a deeper walkthrough, see How to Configure Git Username and Email .

Troubleshooting

E: Unable to locate package git
Run sudo apt update first and verify you are on Debian 13 repositories. If sources were recently changed, refresh package metadata again.

git --version still shows an older version after source install
Your shell may still resolve /usr/bin/git before /usr/local/bin/git. Check with which git and adjust PATH order if needed.

Build fails with missing headers or libraries
One or more dependencies are missing. Re-run the dependency install command and then compile again.

make succeeds but git command is not found
Confirm install step ran successfully: sudo make prefix=/usr/local install. Then check /usr/local/bin/git exists.

FAQ

Should you use apt or source on Debian 13?
For most systems, use apt because updates are integrated with Debian security and package management. Build from source only when you need a newer Git release than the repository version.

Does compiling from source replace the apt package automatically?
No. Source builds under /usr/local and can coexist with the apt package in /usr/bin. Your PATH order determines which binary runs by default.

How can you remove a source-installed Git version?
If you built from the source tree, run sudo make prefix=/usr/local uninstall from that same source directory.

Conclusion

We covered two ways to install Git on Debian 13: using apt, which provides Git 2.47.3, or compiling from source for the latest version. The default repository version is sufficient for most use cases.

For more information, see the Pro Git book .

iOS开发有什么好用的图片浏览器?

年更博主终于推出新版本,JXPhotoBrowser v4.0 全面重构焕新!

JXPhotoBrowser 是一个轻量级、可定制的 iOS 图片/视频浏览器,实现 iOS 系统相册的交互体验。支持缩放、拖拽关闭、自定义转场动画等特性,架构清晰,易于集成和扩展。同时支持 UIKitSwiftUI 两种调用方式(SwiftUI 通过桥接层集成,详见 Demo-SwiftUI 示例工程)。

首页列表 图片浏览 下拉关闭
homepage.png browsing.png pull_down.png

核心设计

  • 零数据模型依赖:框架不定义任何数据模型,业务方完全使用自己的数据结构,通过 delegate 配置 Cell 内容。
  • 图片加载完全开放:框架不内置图片加载逻辑,业务方可自由选择 Kingfisher、SDWebImage 或其他任意图片加载方案。
  • 极简 Cell 协议JXPhotoBrowserCellProtocol 仅包含 browsertransitionImageView 两个属性,将浏览器与具体 Cell 实现解耦,既可以直接使用内置的 JXZoomImageCell,也可以实现完全自定义的 Cell。
  • 协议驱动的数据与 UI 解耦JXPhotoBrowserDelegate 只关心数量、Cell 与转场,不强制统一的数据模型。

功能特性

  • 多模式浏览:支持水平(Horizontal)和垂直(Vertical)两个方向的滚动浏览。
  • 无限循环:支持无限循环滚动(Looping),无缝切换首尾图片。
  • 手势交互
    • 双击缩放:仿系统相册支持双击切换缩放模式。
    • 捏合缩放:支持双指捏合随意缩放(1.0x - 3.0x)。
    • 拖拽关闭:支持下滑手势(Pan)交互式关闭,伴随图片缩小和背景渐变效果。
  • 转场动画
    • Fade:经典的渐隐渐现效果。
    • Zoom:类似微信/系统相册的缩放转场效果,无缝衔接列表与大图。
    • None:无动画直接显示。
  • 浏览体验优化:基于 UICollectionView 复用机制,内存占用低,滑动流畅。
  • 自定义 Cell 支持:内置图片 JXZoomImageCell,也支持通过协议与注册机制接入完全自定义的 Cell(如视频播放 Cell)。
  • Overlay 组件机制:支持按需装载附加 UI 组件(如页码指示器、关闭按钮等),默认不装载任何组件,零开销。内置 JXPageIndicatorOverlay 页码指示器。

核心架构

  • JXPhotoBrowserViewController:核心控制器,继承自 UIViewController。内部维护一个 UICollectionView 用于展示图片页面,负责处理全局配置(如滚动方向、循环模式)和手势交互(如下滑关闭)。
  • JXZoomImageCell:可缩放图片展示单元,继承自 UICollectionViewCell 并实现 JXPhotoBrowserCellProtocol。内部使用 UIScrollView 实现缩放,负责单击、双击等交互。通过 imageView 属性供业务方设置图片。
  • JXImageCell:轻量级图片展示 Cell,不支持缩放手势,适用于 Banner 等嵌入式场景。内置可选的加载指示器(默认不启用),支持样式定制。
  • JXPhotoBrowserCellProtocol:极简 Cell 协议,仅需 browser(弱引用浏览器)和 transitionImageView(转场视图)两个属性即可接入浏览器,另提供 photoBrowserDismissInteractionDidChange 可选方法响应下拉关闭交互,不强制依赖特定基类。
  • JXPhotoBrowserDelegate:代理协议,负责提供总数、Cell 实例、生命周期回调(willDisplay/didEndDisplaying)以及转场动画所需的缩略图视图等,不强制要求统一的数据模型。
  • JXPhotoBrowserOverlay:附加视图组件协议,定义了 setupreloadDatadidChangedPageIndex 三个方法,用于页码指示器、关闭按钮等附加 UI 的统一接入。
  • JXPageIndicatorOverlay:内置页码指示器组件,基于 UIPageControl,支持自定义位置和样式,通过 addOverlay 按需装载。

依赖

  • 框架本身依赖:UIKit(核心),无任何第三方依赖
  • 图片加载:框架不内置图片加载逻辑,业务方可自由选择 Kingfisher、SDWebImage 或其他任意图片加载方案。
  • 示例工程:
    • Demo-UIKit:UIKit 示例,使用 CocoaPods 集成,依赖 Kingfisher 加载图片,演示完整功能(图片浏览、视频播放、Banner 轮播等)。
    • Demo-SwiftUI:SwiftUI 示例,使用 SPM 集成,演示如何通过桥接层在 SwiftUI 中使用 JXPhotoBrowser(媒体网格、设置面板、图片浏览)。
    • Demo-Carthage:UIKit 示例,使用 Carthage 集成。首次使用需在 Demo-Carthage 目录下执行 carthage update --use-xcframeworks --platform iOS 构建框架。

隐私清单(Privacy Manifest)

本框架已包含 PrivacyInfo.xcprivacy 隐私清单文件,符合 Apple 自 2024 年春季起对第三方 SDK 的隐私清单要求。

JXPhotoBrowser 不追踪用户、不收集任何数据、不使用任何 Required Reason API,隐私清单中所有字段均为空声明。通过 CocoaPods、SPM 或 Carthage 集成时,隐私清单会自动包含在框架中,无需额外配置。

系统要求

  • iOS 12.0+
  • Swift 5.4+

安装

CocoaPods

在你的 Podfile 中添加:

pod 'JXPhotoBrowser', '~> 4.0.1'

注意:Xcode 15 起默认开启了 User Script SandboxingENABLE_USER_SCRIPT_SANDBOXING=YES),该沙盒机制会阻止 CocoaPods 的 Run Script 阶段(如 [CP] Copy Pods Resources[CP] Embed Pods Frameworks 等)访问沙盒外的文件,导致编译失败。需要在编译 Target 的 Build Settings 中将 ENABLE_USER_SCRIPT_SANDBOXING 设置为 NO

Target → Build Settings → Build Options → User Script Sandboxing → No

Swift Package Manager

在 Xcode 中:

  1. 选择 File > Add Package Dependencies...
  2. 输入仓库地址:https://github.com/JiongXing/PhotoBrowser
  3. 选择版本规则后点击 Add Package

或在 Package.swift 中添加依赖:

dependencies: [
    .package(url: "https://github.com/JiongXing/PhotoBrowser", from: "4.0.1")
]

Carthage

在你的 Cartfile 中添加:

github "JiongXing/PhotoBrowser"

然后运行:

carthage update --use-xcframeworks --platform iOS

构建完成后,将 Carthage/Build/JXPhotoBrowser.xcframework 拖入 Xcode 工程的 Frameworks, Libraries, and Embedded Content 中,并设置为 Embed & Sign

手动安装

Sources 目录下的所有文件拖入你的工程中。

快速开始

基础用法

import JXPhotoBrowser

// 1. 创建浏览器实例
let browser = JXPhotoBrowserViewController()
browser.delegate = self
browser.initialIndex = indexPath.item // 设置初始索引

// 2. 配置选项(可选)
browser.scrollDirection = .horizontal // 滚动方向
browser.transitionType = .zoom        // 转场动画类型
browser.isLoopingEnabled = true       // 是否开启无限循环

// 3. 展示
browser.present(from: self)

实现 Delegate

遵守 JXPhotoBrowserDelegate 协议,提供数据和转场支持:

import Kingfisher // 示例使用 Kingfisher,可替换为任意图片加载库

extension ViewController: JXPhotoBrowserDelegate {
    // 1. 返回图片总数
    func numberOfItems(in browser: JXPhotoBrowserViewController) -> Int {
        return items.count
    }
    
    // 2. 提供用于展示的 Cell
    func photoBrowser(_ browser: JXPhotoBrowserViewController, cellForItemAt index: Int, at indexPath: IndexPath) -> JXPhotoBrowserAnyCell {
        let cell = browser.dequeueReusableCell(withReuseIdentifier: JXZoomImageCell.reuseIdentifier, for: indexPath) as! JXZoomImageCell
        return cell
    }
    
    // 3. 当 Cell 将要显示时加载图片
    func photoBrowser(_ browser: JXPhotoBrowserViewController, willDisplay cell: JXPhotoBrowserAnyCell, at index: Int) {
        guard let photoCell = cell as? JXZoomImageCell else { return }
        let item = items[index]
        
        // 使用 Kingfisher 加载图片(可替换为 SDWebImage 或其他库)
        let placeholder = ImageCache.default.retrieveImageInMemoryCache(forKey: item.thumbnailURL.absoluteString)
        photoCell.imageView.kf.setImage(with: item.originalURL, placeholder: placeholder) { [weak photoCell] _ in
            photoCell?.setNeedsLayout()
        }
    }
    
    // 4. (可选) Cell 结束显示时清理资源(如取消加载、停止播放等)
    func photoBrowser(_ browser: JXPhotoBrowserViewController, didEndDisplaying cell: JXPhotoBrowserAnyCell, at index: Int) {
        // 可用于取消图片加载、停止视频播放等
    }
    
    // 5. (可选) 支持 Zoom 转场:提供列表中的缩略图视图
    func photoBrowser(_ browser: JXPhotoBrowserViewController, thumbnailViewAt index: Int) -> UIView? {
        let indexPath = IndexPath(item: index, section: 0)
        guard let cell = collectionView.cellForItem(at: indexPath) as? MyCell else { return nil }
        return cell.imageView
    }
    
    // 6. (可选) 控制缩略图显隐,避免 Zoom 转场时视觉重叠
    func photoBrowser(_ browser: JXPhotoBrowserViewController, setThumbnailHidden hidden: Bool, at index: Int) {
        let indexPath = IndexPath(item: index, section: 0)
        if let cell = collectionView.cellForItem(at: indexPath) as? MyCell {
            cell.imageView.isHidden = hidden
        }
    }
    
    // 7. (可选) 自定义 Cell 尺寸,默认使用浏览器全屏尺寸
    func photoBrowser(_ browser: JXPhotoBrowserViewController, sizeForItemAt index: Int) -> CGSize? {
        return nil // 返回 nil 使用默认尺寸
    }
}

在 SwiftUI 中使用

JXPhotoBrowser 是基于 UIKit 的框架,在 SwiftUI 项目中可通过桥接方式集成。Demo-SwiftUI 示例工程演示了完整的集成方案。

核心思路

  1. 网格和设置面板使用纯 SwiftUI 实现(LazyVGridPickerAsyncImage 等)
  2. 全屏图片浏览器通过桥接层调用 JXPhotoBrowserViewController
  3. 创建一个 Presenter 类实现 JXPhotoBrowserDelegate,获取当前 UIViewController 后调用 browser.present(from:)

桥接层示例

import JXPhotoBrowser

/// 封装 JXPhotoBrowserViewController 的创建、配置和呈现
final class PhotoBrowserPresenter: JXPhotoBrowserDelegate {
    private let items: [MyMediaItem]

    func present(initialIndex: Int) {
        guard let viewController = topViewController() else { return }

        let browser = JXPhotoBrowserViewController()
        browser.delegate = self
        browser.initialIndex = initialIndex
        browser.transitionType = .fade
        browser.addOverlay(JXPageIndicatorOverlay())
        browser.present(from: viewController)
    }

    func numberOfItems(in browser: JXPhotoBrowserViewController) -> Int {
        items.count
    }

    func photoBrowser(_ browser: JXPhotoBrowserViewController, cellForItemAt index: Int, at indexPath: IndexPath) -> JXPhotoBrowserAnyCell {
        browser.dequeueReusableCell(withReuseIdentifier: JXZoomImageCell.reuseIdentifier, for: indexPath) as! JXZoomImageCell
    }

    func photoBrowser(_ browser: JXPhotoBrowserViewController, willDisplay cell: JXPhotoBrowserAnyCell, at index: Int) {
        guard let photoCell = cell as? JXZoomImageCell else { return }
        // 加载图片到 photoCell.imageView ...
    }
}

在 SwiftUI View 中调用

struct ContentView: View {
    // 持有 presenter(JXPhotoBrowserViewController.delegate 为 weak,需要外部强引用)
    @State private var presenter: PhotoBrowserPresenter?

    var body: some View {
        LazyVGrid(columns: columns) {
            ForEach(Array(items.enumerated()), id: \.element.id) { index, item in
                AsyncImage(url: item.thumbnailURL)
                    .onTapGesture {
                        let p = PhotoBrowserPresenter(items: items)
                        presenter = p
                        p.present(initialIndex: index)
                    }
            }
        }
    }
}

注意JXPhotoBrowserViewControllerdelegateweak 引用,必须在 SwiftUI 侧用 @State 持有 Presenter 实例,否则它会在创建后立即被释放。

关于 Zoom 转场

Demo-SwiftUI 示例工程未演示 Zoom 转场动画,默认使用 Fade 转场。

原因:Zoom 转场依赖 thumbnailViewAt delegate 方法返回列表中缩略图的 UIView 引用,框架通过该引用计算动画起止位置并构建临时动画视图。而 SwiftUI 的 AsyncImage 等原生视图无法直接提供底层 UIView 引用。

如需自行实现:可将缩略图从 AsyncImage 替换为 UIViewRepresentable 包裹的 UIImageView,从而获取真实的 UIView 引用,再通过 thumbnailViewAtsetThumbnailHidden 两个 delegate 方法提供给框架即可。具体的 Zoom 转场接入方式可参考 Demo-UIKit 示例工程。

JXImageCell 加载指示器

JXImageCell 内置了一个 UIActivityIndicatorView 加载指示器,默认不启用。适用于 Banner 等嵌入式场景下展示图片加载状态。

启用加载指示器

let cell = browser.dequeueReusableCell(withReuseIdentifier: JXImageCell.reuseIdentifier, for: indexPath) as! JXImageCell

// 启用加载指示器
cell.isLoadingIndicatorEnabled = true
cell.startLoading()

// 图片加载完成后停止
cell.imageView.kf.setImage(with: imageURL) { [weak cell] _ in
    cell?.stopLoading()
}

自定义样式

通过 loadingIndicator 属性可直接定制指示器的外观:

cell.loadingIndicator.style = .large       // 指示器尺寸
cell.loadingIndicator.color = .systemBlue  // 指示器颜色

自定义 Cell

框架支持两种方式创建自定义 Cell:

方式一:继承 JXZoomImageCell(推荐)

继承 JXZoomImageCell 可自动获得缩放、转场、手势等功能。以 Demo 中的 VideoPlayerCell 为例,它继承 JXZoomImageCell 并添加了视频播放能力:

class VideoPlayerCell: JXZoomImageCell {
    static let videoReuseIdentifier = "VideoPlayerCell"
    
    private var player: AVPlayer?
    private var playerLayer: AVPlayerLayer?
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        // 自定义初始化:添加 loading 指示器等
    }
    
    /// 配置视频资源
    func configure(videoURL: URL, coverImage: UIImage? = nil) {
        imageView.image = coverImage
        // 创建播放器并开始播放...
    }
    
    /// 重写单击手势:暂停视频或关闭浏览器
    override func handleSingleTap(_ gesture: UITapGestureRecognizer) {
        if isPlaying {
            pauseVideo()
        } else {
            browser?.dismissSelf()
        }
    }
}

方式二:实现协议(完全自定义)

直接实现 JXPhotoBrowserCellProtocol 协议,获得完全的自由度:

class StandaloneCell: UICollectionViewCell, JXPhotoBrowserCellProtocol {
    static let reuseIdentifier = "StandaloneCell"
    
    // 必须实现:弱引用浏览器(避免循环引用)
    weak var browser: JXPhotoBrowserViewController?
    
    // 可选实现:用于 Zoom 转场动画,返回 nil 则使用 Fade 动画
    var transitionImageView: UIImageView? { imageView }
    
    let imageView = UIImageView()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        // 自定义初始化
    }
    
    // 可选实现:下拉关闭交互状态变化时调用
    // isInteracting 为 true 表示用户正在下拉(图片缩小跟随手指),false 表示交互结束(回弹恢复)
    // 适用于在拖拽关闭过程中暂停视频、隐藏附加 UI 等场景
    func photoBrowserDismissInteractionDidChange(isInteracting: Bool) {
        // 例如:下拉时暂停视频播放
    }
}

注册和使用自定义 Cell

let browser = JXPhotoBrowserViewController()

// 注册自定义 Cell(必须在设置 delegate 之前)
browser.register(VideoPlayerCell.self, forReuseIdentifier: VideoPlayerCell.videoReuseIdentifier)

browser.delegate = self
browser.present(from: self)

// 在 delegate 中使用
func photoBrowser(_ browser: JXPhotoBrowserViewController, cellForItemAt index: Int, at indexPath: IndexPath) -> JXPhotoBrowserAnyCell {
    let cell = browser.dequeueReusableCell(withReuseIdentifier: VideoPlayerCell.videoReuseIdentifier, for: indexPath) as! VideoPlayerCell
    cell.configure(videoURL: url, coverImage: thumbnail)
    return cell
}

Overlay 组件

框架提供了通用的 Overlay 组件机制,用于在浏览器上层叠加附加 UI(如页码指示器、关闭按钮、标题栏等)。默认不装载任何 Overlay,业务方按需装载

使用内置页码指示器

框架内置了 JXPageIndicatorOverlay(基于 UIPageControl),一行代码即可装载:

let browser = JXPhotoBrowserViewController()
browser.addOverlay(JXPageIndicatorOverlay())

支持自定义位置和样式:

let indicator = JXPageIndicatorOverlay()
indicator.position = .bottom(padding: 20)  // 位置:底部距离 20pt(也支持 .top)
indicator.hidesForSinglePage = true         // 仅一页时自动隐藏
indicator.pageControl.currentPageIndicatorTintColor = .white
indicator.pageControl.pageIndicatorTintColor = .lightGray
browser.addOverlay(indicator)

自定义 Overlay

实现 JXPhotoBrowserOverlay 协议即可创建自定义组件:

class CloseButtonOverlay: UIView, JXPhotoBrowserOverlay {
    
    func setup(with browser: JXPhotoBrowserViewController) {
        // 在此完成布局(如添加约束)
    }
    
    func reloadData(numberOfItems: Int, pageIndex: Int) {
        // 数据或布局变化时更新
    }
    
    func didChangedPageIndex(_ index: Int) {
        // 页码变化时更新
    }
}

// 装载
browser.addOverlay(CloseButtonOverlay())

多个 Overlay 可同时装载,互不干扰:

browser.addOverlay(JXPageIndicatorOverlay())
browser.addOverlay(CloseButtonOverlay())

保存图片/视频到相册

框架本身不内置保存功能,业务方可自行实现。Demo 中演示了通过长按手势弹出 ActionSheet 保存媒体到系统相册的完整流程。

前提:需要在 Info.plist 中配置 NSPhotoLibraryAddUsageDescription(写入相册权限描述)。

核心步骤

  1. 添加长按手势:在自定义 Cell 中添加 UILongPressGestureRecognizer
  2. 弹出 ActionSheet:通过 browser 属性获取浏览器控制器来 present。
  3. 请求权限并保存:使用 PHPhotoLibrary 请求权限,下载后写入相册。

示例:在自定义 Cell 中长按保存

以 Demo 中的 VideoPlayerCell 为例,继承 JXZoomImageCell 后添加长按保存能力:

import Photos

class VideoPlayerCell: JXZoomImageCell {
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        // 添加长按手势
        let longPress = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress(_:)))
        scrollView.addGestureRecognizer(longPress)
    }
    
    @objc private func handleLongPress(_ gesture: UILongPressGestureRecognizer) {
        guard gesture.state == .began else { return }
        
        let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
        alert.addAction(UIAlertAction(title: "保存视频", style: .default) { [weak self] _ in
            self?.saveVideoToAlbum()
        })
        alert.addAction(UIAlertAction(title: "取消", style: .cancel))
        
        // 通过 browser 属性获取浏览器控制器来 present
        browser?.present(alert, animated: true)
    }
    
    private func saveVideoToAlbum() {
        guard let url = videoURL else { return }
        
        // 1. 请求相册写入权限
        PHPhotoLibrary.requestAuthorization(for: .addOnly) { status in
            guard status == .authorized || status == .limited else { return }
            
            // 2. 下载视频(远程 URL 需先下载到本地)
            URLSession.shared.downloadTask(with: url) { tempURL, _, _ in
                guard let tempURL else { return }
                
                // 3. 写入相册
                PHPhotoLibrary.shared().performChanges({
                    PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: tempURL)
                }) { success, error in
                    // 处理结果...
                }
            }.resume()
        }
    }
}

保存图片的流程类似,将下载部分替换为图片写入即可:

// 下载图片数据
URLSession.shared.dataTask(with: imageURL) { data, _, _ in
    guard let data, let image = UIImage(data: data) else { return }
    
    PHPhotoLibrary.shared().performChanges({
        PHAssetChangeRequest.creationRequestForAsset(from: image)
    }) { success, error in
        // 处理结果...
    }
}.resume()

常见问题 (FAQ)

Q: Zoom 转场动画时图片尺寸不对或有闪烁现象?

A: 这通常是因为打开浏览器时,目标 Cell 的 imageView 还没有设置图片,导致其 bounds 为 zero。

解决方案:在 willDisplay 代理方法中,确保同步设置占位图。例如使用 Kingfisher 时:

func photoBrowser(_ browser: JXPhotoBrowserViewController, willDisplay cell: JXPhotoBrowserAnyCell, at index: Int) {
    guard let photoCell = cell as? JXZoomImageCell else { return }
    
    // 同步从缓存取出缩略图作为占位图
    let placeholder = ImageCache.default.retrieveImageInMemoryCache(forKey: thumbnailURL.absoluteString)
    photoCell.imageView.kf.setImage(with: imageURL, placeholder: placeholder) { [weak photoCell] _ in
        photoCell?.setNeedsLayout()
    }
}

这样可以确保转场动画开始时,Cell 已经有正确尺寸的图片,动画效果更加流畅。

项目开源地址

github.com/JiongXing/P…

How to Revert a Commit in Git

The git revert command creates a new commit that undoes the changes introduced by a specified commit. Unlike git reset , which rewrites the commit history, git revert preserves the full history and is the safe way to undo changes that have already been pushed to a shared repository.

This guide explains how to use git revert to undo one or more commits with practical examples.

Quick Reference

Task Command
Revert the last commit git revert HEAD
Revert without opening editor git revert --no-edit HEAD
Revert a specific commit git revert COMMIT_HASH
Revert without committing git revert --no-commit HEAD
Revert a range of commits git revert --no-commit HEAD~3..HEAD
Revert a merge commit git revert -m 1 MERGE_COMMIT_HASH
Abort a revert in progress git revert --abort
Continue after resolving conflicts git revert --continue

Syntax

The general syntax for the git revert command is:

Terminal
git revert [OPTIONS] COMMIT
  • OPTIONS — Flags that modify the behavior of the command.
  • COMMIT — The commit hash or reference to revert.

git revert vs git reset

Before diving into examples, it is important to understand the difference between git revert and git reset, as they serve different purposes:

  • git revert — Creates a new commit that undoes the changes from a previous commit. The original commit remains in the history. This is safe to use on branches that have been pushed to a remote repository.
  • git reset — Moves the HEAD pointer backward, effectively removing commits from the history. This rewrites the commit history and should not be used on shared branches without coordinating with your team.

As a general rule, use git revert for commits that have already been pushed, and git reset for local commits that have not been shared.

Reverting the Last Commit

To revert the most recent commit, run git revert followed by HEAD:

Terminal
git revert HEAD

Git will open your default text editor so you can edit the revert commit message. The default message looks like this:

plain
Revert "Original commit message"

This reverts commit abc1234def5678...

Save and close the editor to complete the revert. Git will create a new commit that reverses the changes from the last commit.

To verify the revert, use git log to see the new revert commit in the history:

Terminal
git log --oneline -3
output
a1b2c3d (HEAD -> main) Revert "Add new feature"
f4e5d6c Add new feature
b7a8c9d Update configuration

The original commit (f4e5d6c) remains in the history, and the new revert commit (a1b2c3d) undoes its changes.

Reverting a Specific Commit

You do not have to revert the most recent commit. You can revert any commit in the history by specifying its commit hash.

First, find the commit hash using git log:

Terminal
git log --oneline
output
a1b2c3d (HEAD -> main) Update README
f4e5d6c Add login feature
b7a8c9d Fix navigation bug
e0f1a2b Add search functionality

To revert the “Fix navigation bug” commit, pass its hash to git revert:

Terminal
git revert b7a8c9d

Git will create a new commit that undoes only the changes introduced in commit b7a8c9d, leaving all other commits intact.

Reverting Without Opening an Editor

If you want to use the default revert commit message without opening an editor, use the --no-edit option:

Terminal
git revert --no-edit HEAD
output
[main d5e6f7a] Revert "Add new feature"
2 files changed, 0 insertions(+), 15 deletions(-)

This is useful when scripting or when you do not need a custom commit message.

Reverting Without Committing

By default, git revert automatically creates a new commit. If you want to stage the reverted changes without committing them, use the --no-commit (or -n) option:

Terminal
git revert --no-commit HEAD

The changes will be applied to the working directory and staging area, but no commit is created. You can then review the changes, make additional modifications, and commit manually:

Terminal
git status
git commit -m "Revert feature and clean up related code"

This is useful when you want to combine the revert with other changes in a single commit.

Reverting Multiple Commits

Reverting a Range of Commits

To revert a range of consecutive commits, specify the range using the .. notation:

Terminal
git revert --no-commit HEAD~3..HEAD

This reverts the last three commits. The --no-commit option stages all the reverted changes without creating individual revert commits, allowing you to commit them as a single revert:

Terminal
git commit -m "Revert last three commits"

Without --no-commit, Git will create a separate revert commit for each commit in the range.

Reverting Multiple Individual Commits

To revert multiple specific (non-consecutive) commits, list them one after another:

Terminal
git revert --no-commit abc1234 def5678 ghi9012
git commit -m "Revert selected commits"

Reverting a Merge Commit

Merge commits have two parent commits, so Git needs to know which parent to revert to. Use the -m option followed by the parent number (usually 1 for the branch you merged into):

Terminal
git revert -m 1 MERGE_COMMIT_HASH

In the following example, we revert a merge commit and keep the main branch as the base:

Terminal
git revert -m 1 a1b2c3d
  • -m 1 — Tells Git to use the first parent (the branch the merge was made into, typically main or master) as the base.
  • -m 2 — Would use the second parent (the branch that was merged in).

You can check the parents of a merge commit using:

Terminal
git log --oneline --graph

Handling Conflicts During Revert

If the code has changed since the original commit, Git may encounter conflicts when applying the revert. When this happens, Git will pause the revert and show conflicting files:

output
CONFLICT (content): Merge conflict in src/app.js
error: could not revert abc1234... Add new feature
hint: After resolving the conflicts, mark the corrected paths
hint: with 'git add <paths>' and run 'git revert --continue'.

To resolve the conflict:

  1. Open the conflicting files and resolve the merge conflicts manually.

  2. Stage the resolved files:

    Terminal
    git add src/app.js
  3. Continue the revert:

    Terminal
    git revert --continue

If you decide not to proceed with the revert, you can abort it:

Terminal
git revert --abort

This restores the repository to the state before you started the revert.

Pushing a Revert to a Remote Repository

Since git revert creates a new commit rather than rewriting history, you can safely push it to a shared repository using a regular push:

Terminal
git push origin main

There is no need for --force because the commit history is not rewritten.

Common Options

The git revert command accepts several options:

  • --no-edit — Use the default commit message without opening an editor.
  • --no-commit (-n) — Apply the revert to the working directory and index without creating a commit.
  • -m parent-number — Specify which parent to use when reverting a merge commit (usually 1).
  • --abort — Cancel the revert operation and return to the pre-revert state.
  • --continue — Continue the revert after resolving conflicts.
  • --skip — Skip the current commit and continue reverting the rest.

Troubleshooting

Revert is in progress
If you see a message that a revert is in progress, either continue with git revert --continue after resolving conflicts or cancel with git revert --abort.

Revert skips a commit
If Git reports that a commit was skipped because it was already applied, it means the changes are already present. You can proceed or use git revert --skip to continue.

FAQ

What is the difference between git revert and git reset?
git revert creates a new commit that undoes changes while preserving the full commit history. git reset moves the HEAD pointer backward and can remove commits from the history. Use git revert for shared branches and git reset for local, unpushed changes.

Can I revert a commit that has already been pushed?
Yes. This is exactly what git revert is designed for. Since it creates a new commit rather than rewriting history, it is safe to push to shared repositories without disrupting other collaborators.

How do I revert a merge commit?
Use the -m option to specify the parent number. For example, git revert -m 1 MERGE_HASH reverts the merge and keeps the first parent (usually the main branch) as the base.

Can I undo a git revert?
Yes. Since a revert is just a regular commit, you can revert the revert commit itself: git revert REVERT_COMMIT_HASH. This effectively re-applies the original changes.

What happens if there are conflicts during a revert?
Git will pause the revert and mark the conflicting files. You need to resolve the conflicts manually, stage the files with git add, and then run git revert --continue to complete the operation.

Conclusion

The git revert command is the safest way to undo changes in a Git repository, especially for commits that have already been pushed. It creates a new commit that reverses the specified changes while keeping the full commit history intact.

If you have any questions, feel free to leave a comment below.

❌