Làm thế nào để tôi thực hiện một cam kết Git trong quá khứ?


222

Tôi đang chuyển đổi mọi thứ sang Git cho mục đích sử dụng cá nhân của riêng tôi và tôi đã tìm thấy một số phiên bản cũ của một tệp đã có trong kho lưu trữ. Làm cách nào để tôi cam kết lịch sử theo đúng thứ tự theo "ngày sửa đổi" của tệp để tôi có lịch sử chính xác của tệp?

Tôi đã nói với một cái gì đó như thế này sẽ làm việc:

git filter-branch --env-filter="GIT_AUTHOR_DATE=... --index-filter "git commit path/to/file --date " --tag-name-filter cat -- --all  

Câu trả lời ngắn gọn và đơn giản: stackoverflow.com/a/34639957/2708266
Yash

33
Tôi tự hỏi nếu mọi người tìm kiếm câu trả lời cho điều này chỉ muốn giữ "chuỗi đóng góp" GitHub của họ liên tục theo cách này
ZitRo

1
@ZitRo có. Và chỉ cần thiết lập git commit --date="xxx day ago" -m "yyy"là đủ cho mục đích đó nếu bất cứ ai đang tự hỏi.
Vlas Sokolov

alexpeattie.com/blog/usiness-with-dates-in-git : nếu tìm kiếm một lời giải thích nhẹ nhàng
thepurpleowl

Câu trả lời:


198

Những lời khuyên bạn đã đưa ra là thiếu sót. Đặt vô điều kiện GIT_AUTHOR_DATE trong một --env-filtersẽ viết lại ngày của mỗi cam kết. Ngoài ra, sẽ là bất thường khi sử dụng git commit bên trong --index-filter.

Bạn đang đối phó với nhiều vấn đề độc lập ở đây.

Chỉ định ngày khác Khác

Mỗi cam kết có hai ngày: ngày tác giả và ngày ủy viên. Bạn có thể ghi đè từng giá trị bằng cách cung cấp các giá trị thông qua các biến môi trường GIT_AUTHOR_DATE và GIT_COMMITTER_DATE cho bất kỳ lệnh nào ghi một cam kết mới. Xem Định dạng ngày của Nhật Bản trong phần git-commit (1) hoặc bên dưới:

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

Lệnh duy nhất viết một cam kết mới trong quá trình sử dụng bình thường là git commit . Nó cũng có một --datetùy chọn cho phép bạn trực tiếp chỉ định ngày tác giả. Việc sử dụng được dự đoán của bạn git filter-branch --env-filtercũng bao gồm sử dụng các biến môi trường được đề cập ở trên (đây là một phần của phần mềm env, sau đó tùy chọn này được đặt tên; xem Tùy chọn Tùy chọn trong git-filter-Branch (1) và lệnh git-commit bên dưới -tree (1) .

Chèn một tệp vào một lịch sử ref

Nếu kho lưu trữ của bạn rất đơn giản (tức là bạn chỉ có một nhánh duy nhất, không có thẻ), thì có lẽ bạn có thể sử dụng git rebase để thực hiện công việc.

Trong các lệnh sau, sử dụng tên đối tượng (hàm băm SHA-1) của cam kết thay vì xóa A. Đừng quên sử dụng một trong các phương thức ghi đè lên ngày của Mt khi bạn chạy cam kết git .

---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

Nếu bạn muốn cập nhật A để bao gồm tệp mới (thay vì tạo một cam kết mới ở nơi nó được thêm vào), thì hãy sử dụng git commit --amendthay vì git commit. Kết quả sẽ như thế này:

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

Ở trên hoạt động miễn là bạn có thể đặt tên cho cam kết nên là cha mẹ của cam kết mới của bạn. Nếu bạn thực sự muốn tập tin mới của bạn được thêm vào thông qua một cam kết gốc mới (không có cha mẹ), thì bạn cần một cái gì đó hơi khác một chút:

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 --orphanlà tương đối mới (Git 1.7.2), nhưng có nhiều cách khác để làm điều tương tự hoạt động trên các phiên bản Git cũ hơn.

Chèn một tập tin vào một Multi- ref Lịch sử

Nếu kho lưu trữ của bạn phức tạp hơn (nghĩa là nó có nhiều hơn một ref (nhánh, thẻ, v.v.)), thì có lẽ bạn sẽ cần phải sử dụng nhánh lọc git . Trước khi sử dụng nhánh lọc git , bạn nên tạo một bản sao lưu toàn bộ kho lưu trữ của mình. Một kho lưu trữ tar đơn giản của toàn bộ cây làm việc của bạn (bao gồm thư mục .git) là đủ. nhánh bộ lọc git thực hiện sao lưu dự phòng, nhưng thường dễ phục hồi hơn từ bộ lọc không hoàn toàn đúng bằng cách xóa.git thư mục và khôi phục nó khỏi bản sao lưu của bạn.

Lưu ý: Các ví dụ dưới đây sử dụng lệnh cấp thấp hơn git update-index --addthay vì git add. Bạn có thể sử dụng git add , nhưng trước tiên bạn cần sao chép tệp từ một số vị trí bên ngoài sang đường dẫn dự kiến ​​( --index-filterchạy lệnh của nó trong GIT_WORK_TREE tạm thời trống).

Nếu bạn muốn tệp mới của mình được thêm vào mỗi cam kết hiện có, thì bạn có thể làm điều này:

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

Tôi thực sự không thấy bất kỳ lý do nào để thay đổi ngày của các cam kết hiện có --env-filter 'GIT_AUTHOR_DATE=…'. Nếu bạn đã sử dụng nó, bạn sẽ làm cho nó có điều kiện để nó sẽ viết lại ngày cho mỗi cam kết.

Nếu bạn muốn tập tin mới của mình chỉ xuất hiện trong các xác nhận sau một số cam kết hiện có (BẠCH A), thì bạn có thể làm điều này:

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

Nếu bạn muốn tệp được thêm thông qua một cam kết mới sẽ được chèn vào giữa lịch sử của bạn, thì bạn sẽ cần phải tạo cam kết mới trước khi sử dụng nhánh bộ lọc git và thêm --parent-filtervào nhánh bộ lọc git :

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

Bạn cũng có thể sắp xếp để tập tin được thêm vào lần đầu tiên trong một cam kết gốc mới: tạo ra cam kết gốc mới của bạn thông qua phương thức mồ côi mồ côi từ phần git rebase (bắt nó vào new_commit), sử dụng vô điều kiện --index-filter--parent-filtertương tự "sed -e \"s/^$/-p $new_commit/\"".


Trong ví dụ đầu tiên về trường hợp sử dụng, "Chèn tệp vào lịch sử đa tham chiếu": Có cách nào để --index-filteráp dụng cho các cam kết được trả về git rev-listkhông? Hiện tại tôi đang thấy bộ lọc chỉ mục được áp dụng cho một tập hợp danh sách phụ. Đánh giá cao bất kỳ cái nhìn sâu sắc.
Nhím

@Hedgehog: Tất cả các ví dụ đa giới thiệu của người dùng sử dụng -- --allđể xử lý tất cả các cam kết có thể truy cập từ bất kỳ ref nào. Ví dụ cuối cùng cho thấy cách chỉ thay đổi một số cam kết nhất định (chỉ cần kiểm tra GIT_COMMIT cho bất cứ điều gì bạn thích). Để chỉ thay đổi danh sách cam kết cụ thể, bạn có thể lưu danh sách trước khi lọc (ví dụ git rev-list … >/tmp/commits_to_rewrite), sau đó kiểm tra tư cách thành viên bên trong bộ lọc (ví dụ if grep -qF "$GIT_COMMIT" /tmp/commits_to_rewrite; then git update-index …). Chính xác thì bạn đang cố đạt được điều gì? Bạn có thể muốn bắt đầu một câu hỏi mới nếu nó quá nhiều để giải thích trong các bình luận.
Chris Johnsen

Trong trường hợp của tôi, tôi có một dự án nhỏ mà tôi muốn đặt dưới sự kiểm soát nguồn. (Tôi đã không làm như vậy, vì đó là một dự án solo và tôi đã không quyết định sử dụng hệ thống nào). "Kiểm soát nguồn" của tôi chỉ là vấn đề sao chép toàn bộ cây dự án của tôi vào một thư mục mới và đổi tên thư mục phiên bản trước. Tôi mới sử dụng Git (sau khi sử dụng SCCS, RCS và một dẫn xuất RCS độc quyền xấu giống với CVS), vì vậy đừng lo lắng về việc nói chuyện với tôi. Tôi có ấn tượng rằng câu trả lời liên quan đến một biến thể của phần "Chèn tệp vào một lịch sử ref". Chính xác?
Steve

1
@Steve: Kịch bản giới thiệu đơn lẻ của YouTube áp dụng nếu lịch sử của bạn là từ một dòng phát triển duy nhất (nghĩa là sẽ hợp lý nếu tất cả các ảnh chụp nhanh được xem là các điểm liên tiếp của một nhánh tuyến tính). Kịch bản đa giới thiệu của người dùng áp dụng nếu bạn có (hoặc đã có) nhiều chi nhánh trong lịch sử của mình. Trừ khi lịch sử của bạn rất phức tạp (các phiên bản nâng cấp trên các nhánh của các ứng dụng khách khác nhau, vẫn duy trì một nhánh bugfix, trong khi công việc mới đã xảy ra trong trò chơi phát triển, v.v.), thì có lẽ bạn đang xem xét một tình huống đơn lẻ của Ref. Tuy nhiên , có vẻ như tình huống của bạn khác với câu hỏi của
LỚN

1
@Steve: Vì bạn có một loạt các snapshot thư mục lịch sử và bạn chưa có kho lưu trữ Git, nên bạn có thể chỉ cần sử dụng import-directories.perltừ Git contrib/(hoặc import-tars.perl, hoặc import-zips.py) để tạo một kho lưu trữ Git mới từ ảnh chụp nhanh của bạn (ngay cả với Dấu thời gian cũ của người Viking). rebase/ Các filter-branchkỹ thuật trong câu trả lời của tôi chỉ cần thiết nếu bạn muốn chèn một tệp đã bị loại bỏ ra khỏi lịch sử của kho lưu trữ hiện có.
Chris Johnsen

119

Bạn có thể tạo cam kết như bình thường, nhưng khi bạn cam kết, hãy đặt các biến môi trường GIT_AUTHOR_DATEGIT_COMMITTER_DATEtheo các mốc thời gian thích hợp.

Tất nhiên, điều này sẽ thực hiện cam kết ở đầu chi nhánh của bạn (nghĩa là, trước cam kết CHÍNH hiện tại). Nếu bạn muốn đẩy nó trở lại xa hơn trong repo, bạn phải có một chút ưa thích. Giả sử bạn có lịch sử này:

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

Và bạn muốn cam kết mới của mình (được đánh dấu là "X") xuất hiện thứ hai :

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

Cách dễ nhất sẽ là phân nhánh từ lần xác nhận đầu tiên, thêm cam kết mới của bạn, sau đó khởi động lại tất cả các cam kết khác trên đầu trang của cam kết mới. Thích như vậy:

$ 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

25
Tôi luôn luôn tìm thấy câu trả lời hay này, nhưng sau đó phải chạy đi tìm định dạng ngày, vì vậy đây là lần tới 'Thứ Sáu 26 tháng 7 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
Có nhiều, ví dụ như thay vì ghi nhớ 2013-07-26T19:32:10: kernel.org/pub/software/scm/git/docs/...
Marian

3
Nhân tiện, phần '-0400' tạo ra sự bù đắp múi giờ. Hãy lưu ý chọn đúng ... vì nếu không, thời gian của bạn sẽ thay đổi dựa trên điều đó. Ví dụ, trong trường hợp của tôi, người sống ở Chicago, tôi đã phải chọn '-0600' (Bắc Mỹ CST). Bạn có thể tìm thấy các mã ở đây: timeanddate.com/time/zones
evaldeslacasa 4/11/2015

2
kể từ ngày cần được lặp lại, tôi thấy việc tạo một biến khác dễ dàng hơn:THE_TIME='2019-03-30T8:20:00 -0500' GIT_AUTHOR_DATE=$THE_TIME GIT_COMMITTER_DATE=$THE_TIME git commit -m 'commit message here'
Frank Henard

118

Tôi biết câu hỏi này khá cũ, nhưng đó là những gì thực sự có hiệu quả với tôi:

git commit --date="10 day ago" -m "Your commit message" 

16
Điều này làm việc hoàn hảo. Tôi rất ngạc nhiên khi --datecũng hỗ trợ định dạng ngày tương đối dễ đọc của con người.
ZitRo

@ZitRo tại sao không 10 ngày?
Alex78191

10
Cảnh báo: git --datesẽ chỉ sửa đổi $ GIT_AUTHOR_DATE ... vì vậy tùy thuộc vào hoàn cảnh, bạn sẽ thấy ngày hiện tại được đính kèm với cam kết ($ GIT_COMMITTER_DATE)
Guido U. Draheim

3
Gắn thẻ vào những gì @ GuidoU.Draheim nói. Bạn có thể kiểm tra chi tiết đầy đủ của một cam kết bằng cách sử dụng git show <commit-hash> --format=fuller. Ở đó, bạn sẽ thấy AuthorDatengày bạn đã chỉ định, nhưng CommitDatelà ngày thực tế của cam kết.
d4nyll

Vì vậy, không có cách nào để thay đổi CommitDate hoặc thực hiện một cam kết hoàn toàn quá khứ? @ d4nyll
Rahul

35

Trong trường hợp của tôi theo thời gian, tôi đã lưu một loạt các phiên bản của myfile như myfile_bak, myfile_old, myfile_2010, sao lưu / myfile, v.v. Tôi muốn đưa lịch sử của myfile vào git bằng cách sử dụng ngày sửa đổi của chúng. Vì vậy, đổi tên cũ nhất thành myfile git add myfile, sau đógit commit --date=(modification date from ls -l) myfile , đổi tên cũ nhất thành myfile, một cam kết git khác với --date, lặp lại ...

Để tự động hóa phần nào, bạn có thể sử dụng shell-foo để lấy thời gian sửa đổi của tệp. Tôi đã bắt đầu với ls -lcut, nhưng stat (1) trực tiếp hơn

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


1
Đẹp. Tôi chắc chắn đã có các tệp từ trước ngày git của tôi mà tôi muốn sử dụng thời gian sửa đổi tệp cho. Tuy nhiên, không phải điều này chỉ đặt ngày cam kết (không phải ngày của tác giả?)
Xeoncross

Từ git-scm.com/docs/git-commit: --date"Ghi đè ngày tác giả được sử dụng trong cam kết." git logNgày của dường như là AuthorDate, git log --pretty=fullerhiển thị cả AuthorDate và CommitDate.
trượt tuyết

16
Các git committùy chọn --datesẽ chỉ thay đổi GIT_AUTHOR_DATE, không phải GIT_COMMITTER_DATE. Như Pro Git Book giải thích: "Tác giả là người ban đầu viết tác phẩm, trong khi người đi làm là người cuối cùng áp dụng tác phẩm." Trong ngữ cảnh của ngày, ngày GIT_AUTHOR_DATElà ngày tệp được thay đổi, trong khi đó GIT_COMMITTER_DATElà ngày được cam kết. Điều quan trọng ở đây là lưu ý rằng theo mặc định, git loghiển thị ngày của tác giả là Ngày Date, nhưng sau đó sử dụng ngày cam kết để lọc khi được cung cấp --sincetùy chọn.
Christopher

Không có tùy chọn -c cho stat trong OS X 10.11.1
thinsoldier 19/2/2016

Tương đương với stat -c %ytrong macOS (và các biến thể BSD khác) là stat -f %m.
người chiến thắng

21

Sau đây là những gì tôi sử dụng để cam kết thay đổi vào foonhững N=1ngày trong quá khứ:

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

Nếu bạn muốn cam kết một ngày thậm chí cũ hơn, hãy nói lại 3 ngày, chỉ cần thay đổi dateđối số : date -v-3d.

Điều đó thực sự hữu ích khi bạn quên cam kết một cái gì đó ngày hôm qua, ví dụ.

CẬP NHẬT : --datecũng chấp nhận các biểu thức như --date "3 days ago"hoặc thậm chí --date "yesterday". Vì vậy, chúng ta có thể giảm nó thành một dòng lệnh:

git add foo ; git commit --date "yesterday" -m "Update"

6
Cảnh báo: git --datesẽ chỉ sửa đổi $ GIT_AUTHOR_DATE ... vì vậy tùy thuộc vào hoàn cảnh, bạn sẽ thấy ngày hiện tại được đính kèm với cam kết ($ GIT_COMMITTER_DATE)
Guido U. Draheim

trộn 2 cách tiếp cận với nhau làm cho phù hợp gần như hoàn hảo:git commit --amend --date="$(stat -c %y fileToCopyMTimeFrom)"
andrej

Tuyệt vời! Lệnh tương tự với ngày có thể đọc được của con ngườigit commit --amend --date="$(date -R -d '2020-06-15 16:31')"
pixelbrackets

16

Trong trường hợp của tôi, trong khi sử dụng tùy chọn --date, quá trình git của tôi bị hỏng. Có thể tôi đã làm một cái gì đó khủng khiếp. Và kết quả là một số tệp index.lock xuất hiện. Vì vậy, tôi đã xóa thủ công các tệp .lock khỏi thư mục .git và thực thi, cho tất cả các tệp đã sửa đổi được cam kết trong các ngày đã qua và lần này nó hoạt động. Thanx cho tất cả các câu trả lời ở đây.

git commit --date="`date --date='2 day ago'`" -am "update"

5
Xem bình luận ở trêngit commit --date . Ngoài ra, thông điệp cam kết ví dụ của bạn nên được sửa đổi để ngăn chặn các tin nhắn một dòng kém như thế này @JstRoRR
Christopher

4
Cảnh báo: git --datesẽ chỉ sửa đổi $ GIT_AUTHOR_DATE ... vì vậy tùy thuộc vào hoàn cảnh, bạn sẽ thấy ngày hiện tại được đính kèm với cam kết ($ GIT_COMMITTER_DATE)
Guido U. Draheim

9

Để thực hiện một cam kết trông giống như nó đã được thực hiện trong quá khứ, bạn phải đặt cả hai GIT_AUTHOR_DATEGIT_COMMITTER_DATE:

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

nơi date -d'...'có thể là ngày chính xác như 2019-01-01 12:00:00hoặc tương đối thích 5 months ago 24 days ago.

Để xem cả hai ngày trong nhật ký git, hãy sử dụng:

git log --pretty=fuller

Điều này cũng hoạt động cho các cam kết hợp nhất:

GIT_AUTHOR_DATE=$(date -d'...') GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE" git merge <branchname> --no-ff

2

Bạn luôn có thể thay đổi ngày trên máy tính của mình, thực hiện cam kết, sau đó thay đổi ngày trở lại và đẩy.


1
Tôi nghĩ đó không phải là cách làm đúng, nó có thể tạo ra một số vấn đề chưa biết
Vino

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.