Reorder git commit history by date
Asked Answered
U

4

15

I have merged 3 different git repos into one now my history looks something like this:

A1-A2-A3-B1-B2-B3-C1-C2-C3

Now i want to re-order all these commits by date. So finally it may be something like C1-A1-A2-B1-C2-... you get the idea

I am ok if I loose all the original SHA commit ids. And of course I cannot do the reordering by hand so I need some script or some ideas how to approach this.

Utricle answered 2/12, 2014 at 8:32 Comment(1)
I really don't know why you accepted the answer that says that no answer exist - and I used one of the other answers which worked great!Housman
D
10

reorder by interactive rebase

Assuming you would like to change the commit order only on one branch (master).

Create a copy of the branch. This step is optional, you may do it directly on master branch.

git checkout -b master2 master

Create a list of all commits. This command will sort them by Author time, oldest first.

git log --pretty='%H %at' | sort -k2 -n | awk '{ print "pick "$1 }' > /tmp/rebase.list

The output file looks like:

pick <hash of C1>
pick <hash of A1>
pick <hash of A2>
pick <hash of B1>
..

Run the interactive rebase going all the way back to the root of the repository

git rebase -i --root

You will be presented with the current list of commits. Delete everything and load the content of rebase.list file (if your editor happens to be vi, issue commands :1,$d and :r /tmp/rebase.list). Save and exit.

After resolving the conflicts, you should end up with master2 branch with all commits reordered.

Dentist answered 26/4, 2020 at 7:18 Comment(1)
I used your method but i had ~3000 commits in the rebase.. so I changed the command to ask me less questions. git rebase -i --root --strategy=recursive --strategy-option=ours --empty=keep --keep-empty, when you have a conflict (because of a deleted file) solve the conflict by accepting "ours"/"yours". continue rebasing until done. to make sure you did everything right - compare the final version with the previous version - if no changes at all - you're done. otherwise just cherry-pick those changes (or reset those files to source branch version) and commit those minor changes.Housman
D
9

This is just an idea, I did not test it for this scenario but I used it (in a different way) to join two Git repositories and keep the original commit dates.

If the history has branches and merges I think it's impossible to re-order them and keep the structure, even manual. The best you can get is a linear history.

Save the commit hashes and the timestamps (%ct = commit date, %at = author date) into a file, sort them by author date:

$ git log --pretty='%H %at %ct' --author-date-order --reverse > /tmp/hashlist

If the order provided by the command above does not satisfy you then force the order by sorting the output using its second field (the author date):

$ git log --pretty='%H %at %ct' | sort -k 2 > /tmp/hashlist

Create a new repository to hold the history ordered by author date. Create an initial commit setting the committer date in the past (before the oldest commit in your repository):

$ GIT_COMMITTER_DATE='2010-01-01 12:00:00' GIT_AUTHOR_DATE='2010-01-01 12:00:00' git commit --allow-empty

Put your own date into the command above.

Add the old repository as a remote into the new one, fetch all its commits. DO NOT set the master branch of the new repo to track the one of the old repo.

Create a script that will cherry pick the provided commit and apply it on top of the current branch, keeping the original author date and commit date:

$ echo 'GIT_AUTHOR_DATE=@$2 GIT_COMMITTER_DATE=@$3 git cherry-pick $1' > /tmp/pick
$ chmod +x /tmp/pick

If you don't want to keep either the original author date or the committer date (or both) then just remove the corresponding assignment from the above command line.

Use the new script with xargs to pick each commit in the selected order and commit it on top of your new master branch.

$ cat /tmp/hashlist | xargs -n 3 /tmp/pick

If everything went well then remove the temporary files created during the process.

$ rm /tmp/hashlist
$ rm /tmp/pick

Remarks:

  • You will get a linear history. The original branches and merges won't be re-created into the new history timeline.
  • The unmerged branches will not be copied at all. You can use git rebase to copy and attach them to the new commits.
  • Even if your repo does not have branches, there is still a high probability to get conflicts on cherry picks; it depends a lot of the changes introduced by the commits in the new order.
  • If it doesn't work you can always remove the new repository and start over (or quit trying); the old repository is not changed.
Depositary answered 2/12, 2014 at 10:47 Comment(5)
Your idea seems logical. The cherry-picks are running now. Hoping for no/minor conflictsUtricle
Got alot of conflicts, so dropped this approach. So I let them A1-A2-A3-B1-B2-B3-C1-C2-C3 like they were after the merge.Utricle
When it creates a diff, git records not only the changed lines but also the context of the change (1 line before and 1 line after the change). It uses these lines when it applies a diff in order to make sure the diff can be applied safely. If the context lines do not match (f.e. because they were added/changed by a commit that was earlier in the original time line but you want to apply later in the modified time line) a conflict appears. Unfortunately resolving the conflicts automatically is not safe. If you get many conflicts then maybe changing the order of commits is just not possible.Depositary
This worked for me nicely. I didn't need initial empty commit, git fetch worked on empty repo and for cherry-pick I added --strategy=recursive --strategy-option=theirs options to avoid merge conflicts. Rearranged multiple repos and it worked with git version 2.23.0 at the time of this comment.Nutriment
seems nice (using --strategy=recursive --strategy-option=theirs), but does not work if there are deleted files, because they result in conflicts that require manual conflict resolution.Hospitality
P
1

a wrapper script for git rebase to sort commits by author date

git-rebase-order-commits-by-author-date.sh

#!/usr/bin/env bash

# git-rebase-order-commits-by-author-date.sh

# https://mcmap.net/q/746696/-reorder-git-commit-history-by-date

if [ $# = 0 ]; then
  echo "usage:" >&2
  echo "  $0 git_rebase_args..." >&2
  echo "" >&2
  echo "examples:" >&2
  echo "  $0 HEAD~5" >&2
  echo "  $0 --root --committer-date-is-author-date" >&2
  exit 1
fi

if [ $# = 1 ] && [ -e "$1" ] && [ "${1: -34}" == "/.git/rebase-merge/git-rebase-todo" ]; then

  # arg is commit list file
  # this script was called via sequence.editor
  todo_path="$1"
  todo_text_1=$(grep "^pick " "$todo_path")
  # sort by column 3
  todo_text_2=$(echo "$todo_text_1" | sort -k3)
  if [ "$todo_text_1" = "$todo_text_2" ]; then
    # no change
    touch $GIT_REBASE_TODO_EMPTY_PATH
    exit
  fi
  echo "$todo_text_2" > "$todo_path"
  exit

fi

# %at = author timestamp
# the todo file will have 3 columns:
# pick $hash $timestamp

export GIT_REBASE_TODO_EMPTY_PATH=$(mktemp -u)

git \
  -c rebase.instructionFormat="%at" \
  -c sequence.editor="$0" \
  rebase -i "$@"

rc=$?

# fix: exit 0 when nothing to do
if [ $rc = 1 ] && [ -e $GIT_REBASE_TODO_EMPTY_PATH ]; then
  rc=0
  echo "ok: nothing to do"
fi

if [ -e $GIT_REBASE_TODO_EMPTY_PATH ]; then
  rm $GIT_REBASE_TODO_EMPTY_PATH
fi

exit $rc

todo: preserve all committer data to produce a pretty commit history on github.
better than --committer-date-is-author-date

Prose answered 20/3 at 10:23 Comment(0)
T
-6

You can't rearrange A1-A2-A3-B1-B2-B3-C1-C2-C3 without doing it manually

Tynes answered 2/12, 2014 at 9:4 Comment(1)
That is wrong. This is exactly what the rebase command does.Jalopy

© 2022 - 2024 — McMap. All rights reserved.