鴻渊 发表于 2022-4-2 16:06

git rebase 修改commit时间

### 废话
最近碰到一个需求,就是需要修改git项目最早commit时间,全部都往前推一段时间,身为渣渣的我,经历了几天几夜的奋斗,终于搞定了,特来分享一波,要是有发现哪里漏的,可以偷偷告诉我下。
### 步骤
git rebase 命令可以修改commit信息 包括时间、作者、描述等。特以此入手。下文A表示需要变基的分支

- 翻阅资料后,准备了一个不成熟的 shell 脚本,用于实现自动处理,脚本存在不完善部分,主要是缺少测试数据,有想法的可以自己补充一波
- 另 需注意 windows环境下 `GIT_COMMITTER_DATE`命令无法执行,mac的不确定,毕竟穷人买不起,window环境下需在 `git bash` 中执行
- 通过 `_git log > /C/Users/hong/Desktop/log.txt_`_ _ 将A的git日志导出到log.txt文件
- 通过如下java代码 提取 log.txt 文件内容,同时自动生成待处理的 commit 脚本文件
   - 考虑到部分提交存在合并的情况,因而 rebase 时 需要处理的commit可能会跟log.txt顺序不一致,因而考虑生成多个commit 脚本文件 循环调用执行
- 若想从首次提交开始变基 则使用 `git rebase -i --root`,若只是想修改某个区间(注:该区间指定的是一个前开后闭的区间)的可使用 `git rebase -i `,Emm 后面这种方式虽然可以变基,但是奈何能力太浅,变基后生成新分支之后我就不知道怎么覆盖到原分支了,有知道的大佬,有空可以介绍下
   - rebase 会进入可编辑页面,具体使用方式参考 vim
   - 每个commit 正常都是 pick 操作,可进行的修改如下
      - pick:保留该commit(缩写:p)
      - reword:保留该commit,但我需要修改该commit的注释(缩写:r)
      - edit:保留该commit, 但我要停下来修改该提交(不仅仅修改注释)(缩写:e)
      - squash:将该commit和前一个commit合并(缩写:s)
      - fixup:将该commit和前一个commit合并,但我不要保留该提交的注释信息(缩写:f)
      - exec:执行shell命令(缩写:x)
      - drop:我要丢弃该commit(缩写:d)
   - edit即可实现我的需求,因而统一通过`_%s#pick #e #g_`修改为e,并保存, 其中 %s 表示全局替换, 若想按行替换 可改成 1,5s 表示1-5行,根据:set nu 看到的行号进行修改
- 直接在 git bash 上执行通用脚本即可 不需要传入参数
- 确定没问题 执行 git push --force-with-lease 强制更新, 建议强制更新之前备份原分支
### 用到的java代码

```java
// 要往前多少天
int minusDays = 360;
// commit time 往前推操作截止时间
String cutOffTime = "2021-04-21 00:00:00";

@SneakyThrows
@Test
public void readLogFile() {
// 日志文件路径   commit 脚本文件要生成到哪个文件夹中
String filePath = "D:\\log.txt", dirPath = "D:\\gitsh\\";
// 清空文件夹
File file = new File(dirPath);
if (file.exists()) {// 判断是否待删除目录是否存在
    String[] content = file.list();// 取得当前目录下所有文件和文件夹
    assert content != null;
    for(String name : content) {
      File temp = new File(dirPath, name);
      temp.delete();
    }
} else {
    file.mkdir(); // 创建文件夹
}
BufferedReader reader = new BufferedReader(new FileReader(filePath));
String s = null;
List<String> list = new ArrayList<>();
/**
   * log.txt 文件内容格式如下
   * commit c26785080088ff3f146ab438d02736fc90ebb903当前commit hash 一般处理时取前7位
   * Merge: b39b38a 9747c96合并的commit hash需注意 rebase 是无法变更merge的
   * Author: name <email>本次提交的author信息
   * Date:   Tue Dec 14 11:54:41 2021 +0800提交时间
   *      本次提交描述
   *      
   * 提取日志信息
   */
while ((s = reader.readLine()) != null) {
    if (s.startsWith("Author:")) {
      list.add(s.replaceFirst("Author:", "").trim());
    } else if (s.startsWith("Date:")) {
      list.add(dateConvert(s.replaceFirst("Date:", "").trim()));
    } else if (s.startsWith("Merge:")) {
      list.add(s);
    } else if (s.startsWith("commit ")) {
      list.add(s.substring(0, 14));
    }
}
reader.close();

String jb = "GIT_COMMITTER_DATE='%s' GIT_AUTHOR_DATE='%s' git commit --amend --no-edit --date '%s' --author=\"%s\"";

for (int i = 0, len = list.size(); i < len; i += 3) {
    // 因 rebase 无法变基到merge commit,但导出的log.txt中会包含 merge commit 所以需要剔除剔除后生成commit 脚本文件即可
    if (list.get(i + 1).startsWith("Merge:")) {
      i += 1;
      continue;
    }
    if (i + 2 >= len) {
      continue;
    }
    String dateStr = list.get(i + 2), authorStr = list.get(i + 1);

    String[] authors = authorStr.split(" <");

    // 创建文件
    File temp = new File(dirPath, list.get(i).replaceFirst("commit ", "") + ".sh");
    BufferedWriter bw = new BufferedWriter(new FileWriter(temp));
    bw.write("#!/bin/bash");
    bw.newLine();
    // /C/Users/hong/Desktop/rebasejs.sh 上面提到的通用脚本
    bw.write("/C/Users/hong/Desktop/rebasejs.sh c '" + authors + "' '" + authors.substring(0, authors.length() - 1) + "' '" + dateStr + "'");
    bw.newLine();
    // 关闭流
    bw.close();
}
}

@SneakyThrows
public String dateConvert(String dateStr) {
// commit 时间格式处理
SimpleDateFormat sdf = new SimpleDateFormat("EEE MMM dd HH:mm:ss yyyy z", Locale.US);
Date d = sdf.parse(dateStr);
Instant instant = d.toInstant();
ZoneId zone = ZoneId.systemDefault();
LocalDateTime localDateTime = LocalDateTime.ofInstant(instant, zone);
// 在某一些时间之前 提交时间都往前推
if (localDateTime.isBefore(DateUtil.parseDefault(cutOffTime))) {
    localDateTime = localDateTime.minusDays(minusDays);
}
return DateUtil.formatDefault(localDateTime);
}
```

### 通用脚本

```shell
#!/bin/bash
# git rebase 变基使用的脚本 首次调用可不传入参数
# 参数1 当值为 c 时 会自动判断当前变基执行到哪次commit
# 参数2 姓名
# 参数3 邮箱
# 参数4 时间

# 生成的每次commit 脚本文件夹
commit_sh_path=/C/Users/hong/Desktop/gitsh/
# 原本地环境的 author 信息
my_email=xxxx@xxx.com
my_name=xxxx

# 通用处理方法
rebaseCommit() {
        # 临时设置 author 信息为原本次commit提交的author信息
        git config user.name "$1"
        git config user.email "$2"
    # 修改日志信息以及作者信息
        GIT_COMMITTER_DATE="$3" GIT_AUTHOR_DATE="$3" git commit --amend --no-edit --date "$3" --author="$1 <$2>"
        git rebase --continue
        if [ $? -ne 0 ]
    then
            echo '执行失败,开始自动合并'
            conflict
            exit 1
    fi
}
# 冲突自动处理
conflict() {
# 通过 git status 命令可以获得当前 冲突情况
status_res=`git status`
status_res=`echo $status_res | grep -Eo "Unmerged path?:.*[\\.]+" | grep -Eo "(both (modified|deleted|added)|(deleted|added) by (us|them)):\s++" | tail -n 1`
if [ -z "$status_res" ];
then
        echo "合并结束,继续下一个commit"
        checkOver
        exit 1
fi
# 获取冲突类型冲突文件名them表示正要合并的us表示目前的
type_res=`echo $status_res | grep -Eo "(modified|deleted|added)"`
file_name=`echo $status_res | grep -Eo "+" | tail -n 1`
obj_type=`echo $status_res | grep -Eo "(us|them)" | head -n 1`

# 冲突类型分为 both (modified|deleted|added) 和 (modified|deleted|added) by (us|them)
# 目前这几种情况 (modified|deleted|added) by us没碰到过
# 要是各位大佬有碰到的话 可以把 git status 信息发给我参考完善下
if [ "$type_res" == "modified" ];
then
    if [ "$obj_type" == "us" ];
    then
      obj_type="ours"
      # 目前还没碰到当前情况 因而不确定合并方式 碰到会自动提前结束脚本
      echo "发现us: $status_res"
      exit 1
    else
      obj_type="theirs"
    fi
    # 冲突合并方式一般合并 theirs
        git checkout --${obj_type} $file_name
        git add $file_name
elif [ "$type_res" == "deleted" ];
then
    # 冲突是删除文件时使用
        git rm $file_name
elif [ "$type_res" == "added" ];
then
        git add $file_name
else
        echo "无匹配操作: $status_res"
        exit 1
fi
        # 遍历 查看是否还有冲突
        conflict
}
# 检查是否还有需要处理的提交记录
checkOver() {
    # 查看是否存在需要执行的 commit
        status_res=`git status`
        status_res=`echo $status_res | grep -Eo "Last command? done.*(Next|No) command" | grep -Eo "edit {7}" | grep -Eo "{7}$" | tail -n 1`
        if [ ! $status_res ];
        then
                echo "执行完成"
                exit 1
        fi
        echo "执行$status_res"
        $commit_sh_path$status_res.sh
    # 删除已执行文件,方便后续执行完之后查看是否有漏的commit
        rm -rf $commit_sh_path$status_res.sh
}
if [ "$1" == "c" ];
then
# 修改的commit是别人提交时,rebase 会导致出现两个author信息,因而需先取消全局设置的git author信息,等处理完了之后再重新设置
        git config --global --unset user.name
        git config --global --unset user.email
        rebaseCommit "$2" "$3" "$4"
        git config --global user.email "$my_email"
        git config --global user.name "$my_name"
fi

checkOver
```
### 生成的脚本文件
```shell
#!/bin/bash
# 通用脚本 参数1固定参数2表示author名 3表示author邮箱 4表示要改为哪个时间
/D/rebasejs.sh 'c' 'xx' 'xxx@xx.com' '2019-06-30 15:32:04'
```

coolinmind 发表于 2022-4-2 22:00

感谢分享,这个实用{:1_918:}

who4who 发表于 2024-3-14 16:39

感谢,其中commit可以使用--date和--author指定信息,这正是我需要的知识点。
页: [1]
查看完整版本: git rebase 修改commit时间