Как сделать коммит Git в прошлом?

222

Я конвертирую все в Git для личного использования, и я нашел несколько старых версий файла, уже находящихся в хранилище. Как передать его в историю в правильном порядке в соответствии с «датой изменения файла», чтобы у меня была точная история файла?

Мне сказали, что-то вроде этого будет работать:

git filter-branch --env-filter="GIT_AUTHOR_DATE=... --index-filter "git commit path/to/file --date " --tag-name-filter cat -- --all  
неизвестный
источник
Короткие и простые ответы: stackoverflow.com/a/34639957/2708266
Yash
33
Интересно, люди, ищущие ответ на этот вопрос, просто хотят, чтобы их «полоса вклада» GitHub продолжалась таким образом
ZitRo
1
@ZitRo да. А git commit --date="xxx day ago" -m "yyy"для этого достаточно просто настройки , если кому-то интересно.
Влас Соколов
alexpeattie.com/blog/working-with-dates-in-git : если вы ищете нежное объяснение
thepurpleowl
@ZitRo лучше использовать это, чтобы все ваши коммиты в личном проекте происходили на выходных. На всякий случай, если работодатель хочет получить вашу работу, CYA.
Бруно Броноски

Ответы:

198

Совет, который вам дали, ошибочен. Безусловно, установка GIT_AUTHOR_DATE в an --env-filterпереписывает дату каждого коммита. Кроме того, было бы необычно использовать git commit внутри --index-filter.

Вы имеете дело с множеством независимых проблем здесь.

Указание дат, отличных от «сейчас»

Каждый коммит имеет две даты: дату автора и дату коммиттера. Вы можете переопределить каждое из них, указав значения через переменные окружения GIT_AUTHOR_DATE и GIT_COMMITTER_DATE для любой команды, которая записывает новый коммит. Смотрите «Форматы даты» в git-commit (1) или ниже:

Git internal format = <unix timestamp> <time zone offset>, e.g.  1112926393 +0200
RFC 2822            = e.g. Thu, 07 Apr 2005 22:13:13 +0200
ISO 8601            = e.g. 2005-04-07T22:13:13

Единственная команда, которая записывает новый коммит при обычном использовании, это git commit . Также имеется --dateопция, позволяющая напрямую указать дату автора. Предполагаемое использование включает в себя git filter-branch --env-filterтакже использование переменных среды, упомянутых выше (они являются частью «env», после которого названа опция; см. «Опции» в git-filter-branch (1) и базовой команде «plumbing» git-commit). -дерево (1) .

Вставка файла в историю ссылок

Если ваш репозиторий очень прост (т.е. у вас есть только одна ветвь, без тегов), то вы, вероятно, можете использовать git rebase для выполнения этой работы.

В следующих командах используйте имя объекта (хэш SHA-1) фиксации вместо «A». Не забудьте использовать один из методов «переопределения даты» при запуске git commit .

---A---B---C---o---o---o   master

git checkout master
git checkout A~0
git add path/to/file
git commit --date='whenever'
git tag ,new-commit -m'delete me later'
git checkout -
git rebase --onto ,new-commit A
git tag -d ,new-commit

---A---N                      (was ",new-commit", but we delete the tag)
        \
         B'---C'---o---o---o   master

Если вы хотите обновить A, чтобы включить новый файл (вместо создания нового коммита, где он был добавлен), используйте git commit --amendвместо git commit. Результат будет выглядеть так:

---A'---B'---C'---o---o---o   master

Вышеуказанное работает до тех пор, пока вы можете назвать коммит, который должен быть родителем вашего нового коммита. Если вы действительно хотите, чтобы ваш новый файл был добавлен с помощью нового корневого коммита (без родителей), то вам нужно что-то немного другое:

B---C---o---o---o   master

git checkout master
git checkout --orphan new-root
git rm -rf .
git add path/to/file
GIT_AUTHOR_DATE='whenever' git commit
git checkout -
git rebase --root --onto new-root
git branch -d new-root

N                       (was new-root, but we deleted it)
 \
  B'---C'---o---o---o   master

git checkout --orphanявляется относительно новым (Git 1.7.2), но есть и другие способы сделать то же самое, что работает на более старых версиях Git.

Вставка файла в историю нескольких ссылок

Если ваш репозиторий более сложный (т. Е. Имеет несколько ссылок (ветки, теги и т. Д.)), То вам, вероятно, потребуется использовать git filter-branch . Перед использованием git filter-branch , вы должны сделать резервную копию всего вашего хранилища. Достаточно простого архива tar всего вашего рабочего дерева (включая каталог .git). git filter-branch делает резервные ссылки, но зачастую проще восстановить не совсем правильную фильтрацию, просто удалив .gitкаталог и восстановив его из резервной копии.

Примечание. В приведенных ниже примерах git update-index --addвместо команды используется команда более низкого уровня git add. Вы можете использовать git add , но сначала вам нужно будет скопировать файл из какого-то внешнего местоположения в ожидаемый путь ( --index-filterего команда запускается во временном пустом GIT_WORK_TREE).

Если вы хотите, чтобы ваш новый файл был добавлен в каждый существующий коммит, то вы можете сделать это:

new_file=$(git hash-object -w path/to/file)
git filter-branch \
  --index-filter \
    'git update-index --add --cacheinfo 100644 '"$new_file"' path/to/file' \
  --tag-name-filter cat \
  -- --all
git reset --hard

Я не вижу смысла менять даты существующих коммитов --env-filter 'GIT_AUTHOR_DATE=…'. Если бы вы использовали его, вы бы сделали его условным, чтобы он переписывал дату для каждого коммита.

Если вы хотите, чтобы ваш новый файл появлялся только в коммитах после некоторого существующего коммита («A»), то вы можете сделать это:

file_path=path/to/file
before_commit=$(git rev-parse --verify A)
file_blob=$(git hash-object -w "$file_path")
git filter-branch \
  --index-filter '

    if x=$(git rev-list -1 "$GIT_COMMIT" --not '"$before_commit"') &&
       test -n "$x"; then
         git update-index --add --cacheinfo 100644 '"$file_blob $file_path"'
    fi

  ' \
  --tag-name-filter cat \
  -- --all
git reset --hard

Если вы хотите, чтобы файл был добавлен с помощью нового коммита, который должен быть вставлен в середину вашей истории, вам нужно будет сгенерировать новый коммит перед использованием git filter-branch и добавить --parent-filterв git filter-branch :

file_path=path/to/file
before_commit=$(git rev-parse --verify A)

git checkout master
git checkout "$before_commit"
git add "$file_path"
git commit --date='whenever'
new_commit=$(git rev-parse --verify HEAD)
file_blob=$(git rev-parse --verify HEAD:"$file_path")
git checkout -

git filter-branch \
  --parent-filter "sed -e s/$before_commit/$new_commit/g" \
  --index-filter '

    if x=$(git rev-list -1 "$GIT_COMMIT" --not '"$new_commit"') &&
       test -n "$x"; then
         git update-index --add --cacheinfo 100644 '"$file_blob $file_path"'
    fi

  ' \
  --tag-name-filter cat \
  -- --all
git reset --hard

Кроме того, можно организовать файл , чтобы быть первым добавлен в новом корне фиксации: создать новый корень совершать через «сиротский» метод из мерзавец Rebase секции (захват его в new_commit), использовать безусловный --index-filterи --parent-filterкак "sed -e \"s/^$/-p $new_commit/\"".

Крис Джонсен
источник
В первом примере варианта использования «Вставка файла в историю с несколькими ссылками»: есть ли способ --index-filterприменить к коммитам, возвращенным git rev-list? В данный момент я вижу индексный фильтр, примененный к подмножеству rev-list. Ценю любое понимание.
Ежик
@Hedgehog: Все примеры «multi-ref» используются -- --allдля обработки всех коммитов, доступных с любого реф. В последнем примере показано, как изменить только определенные коммиты (просто протестируйте GIT_COMMIT для всего, что вам нравится). Чтобы изменить только определенный список коммитов, вы можете сохранить список перед фильтрацией (например git rev-list … >/tmp/commits_to_rewrite), а затем проверить его на принадлежность к фильтру (например, if grep -qF "$GIT_COMMIT" /tmp/commits_to_rewrite; then git update-index …). Что именно вы пытаетесь достичь? Возможно, вы захотите начать новый вопрос, если это слишком много, чтобы объяснить в комментариях.
Крис Джонсен
В моем случае у меня есть небольшой проект, который я собирался поставить под контроль источников. (Я не сделал этого, потому что это сольный проект, и я не решил, какую систему использовать). Мой «контроль исходного кода» был просто вопросом копирования всего дерева моего проекта в новый каталог и переименования каталога предыдущей версии. Я новичок в Git (после того, как ранее использовал SCCS, RCS и некрасивую проприетарную производную RCS, похожую на CVS), так что не беспокойтесь о разговоре со мной. У меня складывается впечатление, что ответ включает в себя вариант раздела «Вставка файла в историю ссылок». Верный?
Стив
1
@Steve: сценарий «один реф» применяется, если ваша история происходила из одной линии разработки (т. Е. Было бы целесообразно, если бы все снимки рассматривались как последовательные точки одной линейной ветви). Сценарий «multi-ref» применяется, если у вас есть (или было) несколько ветвей в вашей истории. Если ваша история не сложна («разветвленные» версии для разных клиентов, поддерживается ветка «исправление ошибок», в то время как новая работа произошла в «разработке» и т. Д.), То вы, вероятно, смотрите на ситуацию с «единственной ссылкой». Тем не менее , похоже, что ваша ситуация отличается от ситуации вопроса ...
Крис Джонсен
1
@Steve: Так как у вас есть серия исторических снимков каталогов - и у вас еще нет репозитория Git - тогда вы, вероятно, можете просто использовать import-directories.perlиз Git contrib/(или import-tars.perl, или import-zips.py...), чтобы создать новый репозиторий Git из ваших снимков (даже с «Старые» временные метки). В rebase/ filter-branchметодах в моем ответе нужны , только если вы хотите , чтобы вставить файл , который был «левый» из истории существующего хранилища.
Крис Джонсен
119

Вы можете создать коммит как обычно, но когда вы фиксируете, установите переменные среды GIT_AUTHOR_DATEи GIT_COMMITTER_DATEсоответствующие даты и время.

Конечно, это сделает коммит на кончике вашей ветви (т.е. перед текущим коммитом HEAD). Если вы хотите отодвинуть его дальше в репо, вам нужно немного придумать. Допустим, у вас есть эта история:

o--o--o--o--o

И вы хотите, чтобы ваш новый коммит (помеченный как «X») появился вторым :

o--X--o--o--o--o

Самый простой способ - это перейти с первого коммита, добавить новый коммит, а затем перебазировать все остальные коммиты поверх нового. Вот так:

$ git checkout -b new_commit $desired_parent_of_new_commit
$ git add new_file
$ GIT_AUTHOR_DATE='your date' GIT_COMMITTER_DATE='your date' git commit -m 'new (old) files'
$ git checkout master
$ git rebase new_commit
$ git branch -d new_commit
mipadi
источник
25
Я всегда нахожу этот хороший ответ, но потом мне нужно спешить, чтобы найти формат даты, так что вот в следующий раз «Пт 26 июля 19:32:10 2013 -0400»
MeBigFatGuy
94
GIT_AUTHOR_DATE='Fri Jul 26 19:32:10 2013 -0400' GIT_COMMITTER_DATE='Fri Jul 26 19:32:10 2013 -0400' git commit
Xeoncross
15
Там же, к примеру , а мнемонические 2013-07-26T19:32:10: kernel.org/pub/software/scm/git/docs/...
Marian
3
Кстати, часть '-0400' определяет смещение часового пояса. Помните о том, чтобы правильно его выбрать ... потому что если нет, ваше время будет меняться в зависимости от этого. Например, в случае меня, который живет в Чикаго, я должен был выбрать «-0600» (Северная Америка CST). Вы можете найти коды здесь: timeanddate.com/time/zones
evaldeslacasa
3
поскольку дату нужно повторить, мне было проще создать еще одну переменную:THE_TIME='2019-03-30T8:20:00 -0500' GIT_AUTHOR_DATE=$THE_TIME GIT_COMMITTER_DATE=$THE_TIME git commit -m 'commit message here'
Фрэнк Хенард,
119

Я знаю, что этот вопрос довольно старый, но это то, что на самом деле работает для меня:

git commit --date="10 day ago" -m "Your commit message" 
Хайдер Б.
источник
16
Это сработало отлично. Я удивлен, что --dateтакже поддерживает удобочитаемый формат даты.
ZitRo
@ZitRo, почему не 10 дней?
Alex78191
10
Предупреждение: git --dateтолько изменит $ GIT_AUTHOR_DATE ... поэтому в зависимости от обстоятельств вы увидите текущую дату, прикрепленную к коммиту ($ GIT_COMMITTER_DATE)
Гвидо У. Драгхайм
3
Привязка к тому, что сказал @ GuidoU.Draheim. Вы можете проверить полную информацию о коммите, используя git show <commit-hash> --format=fuller. Там вы увидите AuthorDateуказанную вами дату, но CommitDateфактическую дату принятия.
d4nyll
Таким образом, нет никакого способа изменить CommitDate или сделать полностью устаревший коммит? @ d4nyll
Рахул
35

В моем случае со временем я сохранил несколько версий myfile как myfile_bak, myfile_old, myfile_2010, backups / myfile и т. Д. Я хотел поместить историю myfile в git, используя даты их изменения. Так что переименуйте самый старый в myfile, git add myfileзатем git commit --date=(modification date from ls -l) myfileпереименуйте следующий самый старый в myfile, еще один git commit с --date, repeat ...

Чтобы немного это автоматизировать, вы можете использовать shell-foo, чтобы получить время модификации файла. Я начал с ls -lи cut, но stat (1) является более прямым

git commit --date = "stat -c% y myfile` " myfile

skierpage
источник
1
Ницца. У меня наверняка были файлы до моего git дней , для которых я хотел использовать время модификации файла. Однако разве это не только устанавливает дату фиксации (не дату автора?)
Xeoncross
От git-scm.com/docs/git-commit: --date«Переопределить дату автора, использованную в коммите ». git logДата, похоже, является AuthorDate, git log --pretty=fullerпоказывает как AuthorDate, так и CommitDate.
skierpage
16
git commitПараметр --dateбудет изменять только GIT_AUTHOR_DATEне GIT_COMMITTER_DATE. Как объясняет Pro Git Book : «Автор - это человек, который изначально написал произведение, а автор, который в последний раз применил это произведение». В контексте дат, GIT_AUTHOR_DATEэто дата, когда файл был изменен, тогда как GIT_COMMITTER_DATEэто дата, когда он был зафиксирован. Здесь важно отметить, что по умолчанию git logдаты автора отображаются как «Дата», но затем используются даты фиксации для фильтрации при наличии --sinceопции.
Кристофер
Нет опции -c для stat в OS X 10.11.1
thinsoldier
Эквивалент для stat -c %yв macOS (и других вариантах BSD) есть stat -f %m.
победитель
21

Ниже то , что я использую , чтобы вносить изменения в fooв N=1дни в прошлом:

git add foo
git commit -m "Update foo"
git commit --amend --date="$(date -v-1d)"

Если вы хотите взять на себя обязательство еще более старую дату, скажем , 3 дня назад, просто изменить dateаргумент: date -v-3d.

Это действительно полезно, например, если вы забыли что-то сделать вчера.

ОБНОВЛЕНИЕ : --dateтакже принимает выражения как --date "3 days ago"или даже --date "yesterday". Таким образом, мы можем сократить его до одной строковой команды:

git add foo ; git commit --date "yesterday" -m "Update"
Вилсон Виейра
источник
6
Предупреждение: git --dateбудет изменяться только $ GIT_AUTHOR_DATE ... поэтому в зависимости от обстоятельств вы увидите текущую дату, прикрепленную к коммиту ($ GIT_COMMITTER_DATE)
Гвидо У. Драгхайм
смешивание двух подходов делает практически идеальную посадку:git commit --amend --date="$(stat -c %y fileToCopyMTimeFrom)"
андрей
Потрясающие! Та же команда с удобочитаемой датойgit commit --amend --date="$(date -R -d '2020-06-15 16:31')"
пиксельные скобки
16

В моем случае при использовании параметра --date мой процесс git потерпел крах. Может быть, я сделал что-то ужасное. И в результате появился файл index.lock. Поэтому я вручную удалил файлы .lock из папки .git и выполнил их, чтобы все измененные файлы были переданы в прошедшие даты, и на этот раз это сработало. Спасибо за все ответы здесь.

git commit --date="`date --date='2 day ago'`" -am "update"
JstRoRR
источник
5
Смотрите комментарий вышеgit commit --date . Кроме того , ваш пример сообщения о фиксации должны быть внесены изменения , чтобы препятствовать плохие сообщения однострочные , как эти @JstRoRR
Christopher
4
Предупреждение: git --dateтолько изменит $ GIT_AUTHOR_DATE ... поэтому в зависимости от обстоятельств вы увидите текущую дату, прикрепленную к коммиту ($ GIT_COMMITTER_DATE)
Гвидо У. Драгхайм
9

Чтобы сделать коммит, который выглядит так, как будто это было сделано в прошлом, вы должны установить GIT_AUTHOR_DATEи GIT_COMMITTER_DATE:

GIT_AUTHOR_DATE=$(date -d'...') GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE" git commit -m '...'

где date -d'...'может быть точная дата как 2019-01-01 12:00:00или относительная как 5 months ago 24 days ago.

Чтобы увидеть обе даты в git log, используйте:

git log --pretty=fuller

Это также работает для коммитов слияния:

GIT_AUTHOR_DATE=$(date -d'...') GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE" git merge <branchname> --no-ff
MX0
источник
3

Вы всегда можете изменить дату на своем компьютере, сделать коммит, а затем изменить дату назад и нажать.

Nik
источник
1
Я думаю, что это неправильная практика, это может создать некоторые неизвестные проблемы
Vino
2

Или просто используйте fake-git-history, чтобы сгенерировать ее для определенного диапазона данных.

rel1x
источник