Doom Emacs 配置同步方案分享

最近优化了同步 Doom Emacs 配置的方案,详见 memacs 。该方式也可用于其他与大量提交的项目同步的情况。


Doom 是非常强大的、可配置性极高的 Emacs 配置框架, 用户使用时只需要 clone 它源码到 ~/.config/emacs 目录,再将自己的配置写到 ~/.doom.d 即可。按标准使用 方式,用户只维护 ~/.doom.d 目录下自己的配置。但是,随着用户逐渐深度地使用 Emacs ,Doom 提供的 可配置空间将愈发不足。因此需要一种方式,既能方便地同步 Doom 原项目的更新,又可以随意修改 Doom 任意配置。

本人曾采用 clone 原项目到本地并添加两个远程库(一个为 Doom ,另一个为本人自己的github 仓库)的方式,使用 过程中发现分支提交数量过多,除少量本人提交之外,绝大多数都是原库各位贡献者大佬的提交。在该方式下,大量提交是 一种负担,然而用户仅想区分自身提交与官方改动。为改进此问题,本人设计了一种新的同步方式,以供所需者参考。

方案

核心设计

  1. 识别出 Doom 中比用户项目已同步提交更新的所有提交;
    • 如,Doom主分支提交为 A..G ,用户项目主分支已同步提交为 A..C ,此状态下用户需要同步的更新提交 为 D..G
    • 注,案例中用户项目主分支为用户主要维护分支,已同步提交为用户与 Doom 主分支的同步状态,用户主分支中 不含有 A..C 提交。
  2. 将更新的提交压缩为一个汇总提交,所有提交信息也压缩为汇总提交的提交信息;
    • 即,将 D..G 压缩为 U1
  3. 将汇总提交合并至用户项目主要维护的分支;
    • 即,将压缩后的 U1 合并至用户项目主分支

实现

本节指导如何在已定期同步 Doom 官方提交的用户项目上作调整,从0开始的项目可参考简化。

0.重新克隆 Doom 项目

$ git clone --single-branch https://github.com/hlissner/doom-emacs NEW_PROJECT

1.识别 Doom 中本地项目当前已合并的最新提交并回退

此步骤需人为识别,通过 git log 命令在用户项目中筛选出 Doom 的最新提交,获取提交id,如 8846d151814ebbf7fb90d9d5dd16cd737257408e

在 NEW_PROJECT 中执行回退:

$ cd NEW_PROJECT
$ git reset --hard 8846d151814ebbf7fb90d9d5dd16cd737257408e

2.重建项目git仓库

在 NEW_PROJECT 中清理原git仓库信息,将当前目录中文件内容作为重建后仓库的第一个提交。

$ cd NEW_PROJECT
$ rm -rf .git
$ git init
$ git add --all
$ git commit -am "init: squash to 8846d151814ebbf7fb90d9d5dd16cd737257408e"

3.拷贝原用户项目文件覆盖至新项目中并提交

删除 NEW_PROJECT 中除 .git/ 以外的所有文件与文件夹。

$ cd NEW_PROJECT
$ rm -rf .d* .github .gitignore bin docs early-init.el LICENSE lisp modules profiles README.md shell.nix static

拷贝用户原项目 OLD_PROJECT 中除 .git/ 以外的所有文件与文件夹。此处建议通过手工拖拽形式等形式将非隐藏文件复制到 NEW_PROJECT 文件夹,隐藏文件通过命令拷贝:

$ cp -R ../OLD_PROJECT/.clj-kondo ../OLD_PROJECT/.d* ../OLD_PROJECT/.gitignore ../OLD_PROJECT/.lsp/ .

提交第一个用户改动,该改动压缩了用户历史所有提交。

$ git add --all
$ git commit -am"migrate: squash until 2025.3.31"

4.新建同步分支并回退至用户项目当前已合并的最新提交

新建同步分支,该分支用于压缩提交。

$ cd NEW_PROJECT
$ git checkout -b doom-squash
$ git reset --hard HEAD^
$ git remote add doom https://github.com/hlissner/doom-emacs

5.创建同步状态文件

同步状态文件用于同步脚本识别已同步提交状态,内容为已同步最新提交的提交id。

$ cd NEW_PROJECT
$ mkdir -p .local/doom-branch-sync
$ echo -n "8846d151814ebbf7fb90d9d5dd16cd737257408e" > .local/doom-branch-sync/head

6.创建同步脚本并执行同步

在 NEW_PROJECT 目录外创建同步脚本 auto_merge.sh ,复制下文同步脚本中的内容,并通过 chmod a+x auto_merge.sh 添加执行权限。

执行同步:

$ ./auto_merge.sh ./NEW_PROJECT

同步脚本

#!/bin/bash
#
# 同步doom项目更新代码到用户项目的master分支。doom项目的新提交将被压缩成一个提交。
#
# USAGE:
#   sh auto_merge.sh DIR
#      DIR 为本地 doom emacs 项目所在目录。
#
#

MEMACS_DIR=$1
if [ -z "$MEMACS_DIR" ]; then
    echo "USAGE:"
    echo "  sh auth_merge.sh DIR"
    echo "      DIR 为本地 memacs 项目所在目录。"
    exit 1
fi

if [ ! -d $MEMACS_DIR ] || [ ! -d  $MEMACS_DIR/.git ]; then
    echo "Error: $MEMACS_DIR 不是一个合法 memacs 项目目录。"
    exit 1
fi

# 主项目分支
BRANCH_MASTER=master
# 同步中间分支
BRANCH_SQUASH=doom-squash
# doom远程名
REMOTE_DOOM=doom

LOCAL_DIR=.local/doom-branch-sync

# 获取远程doom的master分支最新提交
function am_get_remote_doom_head_commit() {
    git fetch $REMOTE_DOOM > /dev/null 2>&1 
    git log --pretty=oneline --remotes=$REMOTE_DOOM | head -n 1 | awk '{ print $1 }'
    return $?
}

# 将提交压入历史栈并返回上一次栈顶提交
function am_push_doom_squash_sync_commit() {
    now=$1
    if [ -z "$now" ]; then
        return 1
    fi
    mkdir -p $LOCAL_DIR
    if [ ! -f $LOCAL_DIR/head ]; then
        return 1
    fi

    prev=`cat $LOCAL_DIR/head`
    if [ "$prev" == "$now" ]; then
        # 前后版本相同,无需执行更新
        return 2
    fi
    echo -n "$prev" > $LOCAL_DIR/prev
    echo -n "$now"  > $LOCAL_DIR/head
    echo "$prev"
}

# 安全切换分支
function am_checkout_branch_safely() {
    target_branch=$1
    if [ -z "$target_branch" ]; then
        exit 1
    fi
    git checkout $1
    return $?
}

# 获取远程doom中两个提交之间所有的改动并应用
function am_diff_commits_and_apply_change() {
    prev=$1
    head=$2
    if [ -z "$prev" ]; then
        return 1
    fi
    if [ -z "$head" ]; then
        return 1
    fi
    git diff $prev..$head | git apply --3way
    return $?
}

# 获取远程doom中两个提交之间所有日志并提交当前改动
function am_log_commits_and_commit() {
    prev=$1
    head=$2
    if [ -z "$prev" ]; then
        return 1
    fi
    if [ -z "$head" ]; then
        return 1
    fi

    git log --pretty=oneline --no-decorate --remotes=$REMOTE_DOOM $prev..$head \
        | awk '{$1=""}1' > $LOCAL_DIR/message
    if [ $? -ne 0 ]; then
        return 1
    fi

    git commit -am"squash:$prev..$head
$(cat $LOCAL_DIR/message )"
    return $?
}

# 合并分支
function am_merge() {
    target_branch=$1
    if [ -z "$target_branch" ]; then
        exit 1
    fi
    git merge $target_branch
    return $?
}

cd $MEMACS_DIR
latest_commit=`am_get_remote_doom_head_commit`
if [ $? -ne 0 ]; then
    echo "Error: 未获取到远程doom的master分支最新提交。"
    exit 1
fi

previous_commit=`am_push_doom_squash_sync_commit $latest_commit`
re=$?
if [ $re -ne 0 ]; then
    if [ $re -eq 2 ]; then
        echo "Info: doom未更新,无需同步。"
        exit 0
    fi
    echo "Error: 将提交压入历史栈并返回上一次栈顶提交错误。"
    exit 1
fi

am_checkout_branch_safely $BRANCH_SQUASH
if [ $? -ne 0 ]; then
    echo "Error: 无法切换分支至$BRANCH_SQUASH。"
    exit 1
fi

echo "获取远程doom中 $prev 与 $head 之间所有的改动并应用"
am_diff_commits_and_apply_change $previous_commit $latest_commit
if [ $? -ne 0 ]; then
    echo "Error: 无法获取远程doom中两个提交之间所有的改动并应用。"
    exit 1
fi

am_log_commits_and_commit $previous_commit $latest_commit
if [ $? -ne 0 ]; then
    echo "Error: 无法获取远程doom中两个提交之间所有日志并提交当前改动。"
    exit 1
fi

am_checkout_branch_safely $BRANCH_MASTER
if [ $? -ne 0 ]; then
    echo "Error: 无法切换分支至$BRANCH_MASTER。"
    exit 1
fi

echo "合并 $BRANCH_SQUASH 的改动提交到 $BRANCH_MASTER "
am_merge $BRANCH_SQUASH

欢迎大家学习交流。

能讲下你为什么需要这个复杂的配置管理吗?我不用doom, 直接在init.el里面写配置,大概300多行吧,但是核心的可能就100行左右。

师承子龙山人spacemacs教学,捣鼓2年略有所悟,改换doom精简配置。

为学日益,闻道日损。往后一朝顿悟,便也如你一般,再精再简。

我的配置少可能是因为我就用于工作中写代码,Java, Javascript, typescript 这些。这些东西配置简单。

无论spacemacs还是doom中,我自己用到的都不多,主要是 org 、ivy 、company、magit、evil、workspace、project、各种编程语言,每次拉取更新也就看看这几块改了什么、有什么新特性。

但若自己要写一个全新的功能,这两个以及其他的配置框架,都是很好的学习库和辅助工具。

1 个赞

同步上游库最简单的办法不就是 git pull --rebase 吗?还有就是同样简单的直接 git merge,为什么不用这种最简单的做法呢?

另外,你这么折腾文件系统,删了 .git 目录再重新初始化,跟 Git 的正常用法可以说是南辕北辙了。反正我是从没听说过先 rm -rf .gitgit add . 这种操作的。

和 git rebase 区别为只解决一遍冲突。

使用rebase时,手动压缩完提交后,本地分支历史与远程已不一致,下一次同步远程提交将处理一些额外的冲突内容。长期使用该方式,最终只能重建分支。

那为啥不用 git merge?

不使用未压缩提交的merge的原因已经写在说明里了。

减少非必要的提交数量,减轻仓库大小。

提交的信息是用来记录提交的目的和方便二分法排查 bug 的。

你这样每次把所有 commit 都压缩到一条信息里,尤其是 doom 的 commit 大部分都是东南西北的各个模块东一块西一块的内容。到时候出了 bug 首先没办法用二分法排查,其次没办法通过 blame 等方式去查找有问题的地方的修改原因。

而且 doom 本身的提交信息都是 hlissner 有精心维护的,根本不是那种乱糟糟的提交。

相比提交信息,空间可以说是最不值钱的了。

本场景下,只需区分是自己的bug与官方的bug即可。究竟是哪个贡献者写的bug并不重要。

首先,这个是你自己的个人项目,你自己觉得这样配置最舒服,那当然就是最好的。

不过,既然你来论坛发帖想和大家聊聊,那我就说说我的看法。

只需区分是自己的bug与官方的bug即可

你说“只需区分是自己的 bug 和官方的 bug 就行”,但 Doom 是个开源社区,根本没有所谓的“官方”。它是所有人一起贡献力量的成果。在 Doom 的 issue 列表里,很多人都非常努力地用二分法这类方法去定位 bug 的根源,希望能彻底解决问题。你这样区分“自己的 bug”和“官方的 bug”,感觉和整个开源社区的协作方式有点背道而驰了。

当然,大部分人用开源项目就是直接用,会去动手改进的人本来就不多。但你现在都有能力直接改 Doom 的源代码了,不只是把它当个库来用,那为什么不试着多给社区做点贡献呢?

我的意思是,和你现在的方法比起来,直接用 git merge 能让你在排查 bug 的时候更灵活、更方便,给 Doom 社区贡献代码也更容易一些。我不是说你现在这样做就没法做贡献了,只是觉得有更方便的路子。

1 个赞

酒肉穿肠过,佛祖心中留。 世人若学我,如同进魔道。

emacs大佬们都有自己独有的配置,紧跟Spacemacs和Doom的步伐是便宜法门,却非大道。此方案不过是本人求索路上,一项逐渐与Spacemacs、Doom分道扬镳的副产品。不必太过较真。

我也没听说过,不过有就是了,我就没少这么做