10 Vấn đề về Git thường gặp và Giải pháp

Bất kể ai từ newbie tới “gừng già” đều sẽ học được cái gì đó về Git.

1. Bỏ phần chỉnh sửa local file

Đôi khi cách tốt nhất để nắm được vấn đề là phải “ngụp lặn” mầm mò trong code. Nhưng đáng tiếc là, những thay đổi trong quá trình lại không được tối ưu lắm, trong trường hợp này thì revert file về lại trạng thái ban đầu có lẽ là cách nhanh gọn nhất:

git checkout -- Gemfile # reset specified path 
git checkout -- lib bin # also works with multiple arguments

Dấu 2 gạch (--) biểu thị cho tính năng command line để báo kết thúc các option của command.

2. Undo các local commit

Đôi khi chúng ta phải tốn khá nhiều thời gian để nhận ra rằng mình đang đi sai hướng, và đến lúc đó thì không ít thay đổi đã được áp dụng. Đấy là lúc mà git reset trở nên có ích:

git reset HEAD~2        # undo last two commits, keep changes
git reset --hard HEAD~2 # undo last two commits, discard changes

Hãy cẩn thận với option --hard! Nó sẽ reset hệ thống bạn làm việc, cũng như tất cả các thay đổi cũng sẽ bị mất đi luôn.

3. Remove một file khỏi git mà không phải remove nó khỏi file system

Nếu bạn không cẩn thận khi dùng git add, bạn sẽ add một mớ file không mong muốn vào mà không hay biết. Tuy nhiên, git rm sẽ remove nó trên cả staging area, cũng như file system của bạn – một điều không ai mong muốn. Trong trường hợp này, hãy đảm bảo rằng bạn chỉ remove bản staged thôi, và add file vào .gitignore để tránh lặp lại lỗi lần hai:

git reset filename          # or git remove --cached filename
echo filename >> .gitignore # add it to .gitignore to avoid re-adding it

4. Edit commit message

Lúc này sẽ có typo, nhưng may thay là bạn có thể sửa nó khá dễ trên commit messages:

git commit --amend                  # start $EDITOR to edit the message
git commit --amend -m "New message" # set the new message directly

Nhưng đó vẫn chưa hết công dụng của git-amend. Nếu bạn có quên add một file vào, chỉ cần add vào và chỉnh lại commit trước đó!

git add forgotten_file 
git commit --amend

Hãy nhớ rằng --amend thật ra tạo commit mới thay thế cho cái trước, nên đừng dùng nó để thay đổi commit mà đã được đẩy vào giữa repository. Có một ngoại lệ đó là bạn phải chắc chắn về việc không có developer nào đã check các bản trước và làm dựa trên đó, như thế thì một forced push (git push --force) vẫn sẽ ổn. Cần có option --force ở đây vì lịch sử của hệ thống đã bị điều chỉnh local, nghĩa là push sẽ bị reject bởi server từ xa vì fast-forward merge là không thể.

5. Dọn local commit trước khi push

Mặc dù --amend rất hữu ích, nó không thật sự có tác dụng nếu phần commit bạn muốn  reword không phải là cái cuối. Trong trường hợp này, bạn sẽ cần đến một interactive rebase:

git rebase --interactive 
# if you didn't specify any tracking information for this branch 
# you will have to add upstream and remote branch information: 
git rebase --interactive origin branch

Nó sẽ mở ra phần editor và hiển thị menu sau:

pick 8a20121 Upgrade Ruby version to 2.1.3 
pick 22dcc45 Add some fancy library 
# Rebase fcb7d7c..22dcc45 onto fcb7d7c 
# 
# Commands: # p, pick = use commit 
# r, reword = use commit, but edit the commit message 
# e, edit = use commit, but stop for amending 
# s, squash = use commit, but meld into previous commit 
# f, fixup = like "squash", but discard this commit's log message 
# x, exec = run command (the rest of the line) using shell 
# 
# These lines can be re-ordered; they are executed from top to bottom. 
# 
# If you remove a line here THAT COMMIT WILL BE LOST. 
# 
# However, if you remove everything, the rebase will be aborted. 
#
# Note that empty commits are commented out

Ở phần đầu, bạn sẽ thấy một list các local commit, kèm theo phần giải thích cho các command có sẵn. Chỉ cần chọn phần commit bạn muốn update, đổi pick thành reword (hoặc viết tắt là r), và bạn sẽ có được một view mới nơi bạn có thể edit message.

Tuy nhiên, như có thể thấy từ listing trên, các interactive rebase offer nhiều cái khác nữa ngoài edit commit message: bạn có thể remove commit hoàn toàn bằng cách xoá nó khỏi list, cũng như edit hoặc sắp xếp lại hoặc squash nó. Squash cho phép bạn gộp nhiều commit thành một, một bước tôi hay làm trên các nhánh feature trước khi push chúng vào phần remote. Từ đó sẽ không còn các bản record các commit “Add các file bị miss” và “Sửa typo” nữa!

6. Revert các commit đã push

Mặc dù một số giải pháp đã được nêu rõ trong những tip trước, một số commit sai đôi khi sẽ trội lên hẳn trong repository. Không sao cả, vì git đã cho bạn một cách đơn giản để revert một hoặc nhiều commit:

 git revert c761f5c              # reverts the commit with the specified id
 git revert HEAD^                # reverts the second to last commit
 git revert develop~4..develop~2 # reverts a whole range of commits

Nếu bạn không muốn tạo thêm revert commit mà chỉ muốn áp dụng các thay đổi cần thiết vào hệ thống, bạn có thể dùng option --no-commit/-n.

# undo the last commit, but don't create a revert commit 
git revert -n HEAD

Trang manual tại man 1 git-revert cũng list ra nhiều option khác và cho thêm một số ví dụ khác nữa.

7. Tránh các conflict trùng lặp về merge

Một chuyện mà mọi developer đều hiểu, rằng việc fix merge conflict nghe thật sự kinh dị, nhưng việc cứ xử lý đi xử lý lại một conflict (ví dụ: trong các feature branch chạy lâu dài) thì thật sự là một cơn ác mộng. Nếu bạn đã gặp tình trạng này, bạn sẽ rất vui khi được biết về feature reuse recorded resolution feature. Hãy add nó vào global config cho tất cả các project:

git config --global rerere.enabled true

Ngoài ra, bạn có thể dùng nó trên từng dự án bằng cách tạo thủ công thư mục .git/rr-cache.

Đây không phải là một feature cho tất cả mọi người, nhưng riêng đối với những ai cần nó,  nó sẽ tiết kiệm cho bạn không ít thời gian. Hãy tưởng tượng team của bạn đang làm nhiều nhánh tính năng khác nhau cùng một lúc. Bây giờ bạn muốn hợp nhất tất cả chúng lại với nhau thành một nhánh có thể test được trước khi release. Từ đó bạn sẽ phải xử lý một số conflict về merge. Không may một trong các nhánh vẫn chưa hoàn thiện, vì vậy bạn phải hủy merge lại một lần nữa. Vài ngày (hoặc vài tuần) sau khi nhánh cuối cùng đã sẵn sàng, bạn sẽ merge lại, nhưng nhờ vào các độ phân giải đã ghi, bạn sẽ không phải giải quyết xung đột hợp nhất lần nữa.

Trang chính (man git-rerere) có nhiều info hơn về các case và các command (git rerere statusgit rerere diff, v.v).

8. Tìm commit gây lỗi sau khi merge

Việc mò ra commit đã kéo bug sau khi merge khá tốn thời gian. May mắn là git cung cấp cho chức năng search nhị phân rất hay dưới dạng git-bisect. Đầu tiên bạn cần biểu hiện setup đầu tiên:

git bisect start         # starts the bisecting session
 git bisect bad           # marks the current revision as bad
 git bisect good revision # marks the last known good revision

After this git will automatically checkout a revision halfway between the known “good” and “bad” versions. You can now run your specs again and mark the commit as “good” or “bad” accordingly.

Sau khi git tự động checkout các phiên bản “tốt” và bản “xấu”. Bạn cũng có thể chạy lại các spec và đánh dấu commit tương ứng là “tốt” hay “xấu”.

git bisect good # or git bisec bad

Process này sẽ tiếp tục cho đến khi bạn tìm ra commit mang bug.

9. Tránh các lỗi thường gặp các git hook

Một số lỗi bị lặp lại rất thường xuyên, nhưng sẽ dễ tránh được nếu như check thường xuyên hoặc các task cleanup ở một stage nhất định trong git workflow. Hook được thiết kế để dùng cho trường hợp này. Để tạo ra hook mới, hãy add thêm một file vào .git/hooks. Tên của script phải tương thích với một trong những hook có sẵn , một list đầy đủ những cái có sẵn trên manual page (man githooks). Bạn cũng có thể define các global hook để dùng trong các project của mình bằng cách tạo ra template directory cho git sử dụng khi khởi động một repository mới. Một template directory mẫu trông như sau:

[init]
    templatedir = ~/.git_template

  → tree .git_template
  .git_template
  └── hooks
      └── pre-commit

Khi khởi động initialize một repository mới, các file trong template directory sẽ bị copy về vị trí tương ứng trong  .git directory của project.

Dứoi đây là một hook commit-msg mẫu, để đảm bảo rằng mỗi commit message đều hướng về một ticket number như “#123“.

ruby
  #!/usr/bin/env ruby
  message = File.read(ARGV[0])

  unless message =~ /\s*#\d+/
    puts "[POLICY] Your message did not reference a ticket."
    exit 1
  end

10. Nếu tất cả đều fail

Tới đây thì chúng ta đã cover được khá nhiều cách fix các lỗi thường gặp khi làm việc với git. Hầu hết giải pháp đều khá ổn, tuy nhiên cũng có lúc không dùng được cách dễ và phải viết lại history của nguyên branch. Một cách hay dùng trong trường hợp này đó là remove phần data nhạy cảm (ví dụ như uỷ quyền login cho hệ thống production) gắn liền với public repository:

git filter-branch --force --index-filter \
  'git rm --cached --ignore-unmatch secrets.txt' \
  --prune-empty --tag-name-filter cat -- --all

Việc này sẽ remove file secrets.txt khỏi tất cả các branch và tag. Nó cũng remove mọi commit trống sau khi thực hiện remove. Hãy nhớ rằng việc này cũng sẽ viết lại history của toàn bộ project mà có thể broke trên workflow. Và trong lúc file đang bị remove, phần uỷ quyền vẫn khá nguy hiểm!

GitHub đã có một bài tutorial cách remove data nhạy cảm và man git-filter-branch có mọi chi tiết về những filter và option có sẵn.