Bagaimana Anda memperbaiki gabungan yang buruk, dan memutar ulang komitmen baik Anda ke gabungan tetap?


407

Saya tidak sengaja melakukan file yang tidak diinginkan ( filename.origsaat menyelesaikan penggabungan) ke repositori saya beberapa komit yang lalu, tanpa saya sadari sampai sekarang. Saya ingin sepenuhnya menghapus file dari riwayat repositori.

Apakah mungkin untuk menulis ulang sejarah perubahan sedemikian rupa sehingga filename.origtidak pernah ditambahkan ke repositori?



Jawaban:


297

Tolong jangan gunakan resep ini jika situasi Anda tidak seperti yang dijelaskan dalam pertanyaan. Resep ini untuk memperbaiki gabungan yang buruk, dan memutar ulang komitmen baik Anda ke gabungan tetap.

Meskipun filter-branchakan melakukan apa yang Anda inginkan, ini adalah perintah yang cukup kompleks dan saya mungkin akan memilih untuk melakukan ini git rebase. Itu mungkin preferensi pribadi. filter-branchdapat melakukannya dalam satu perintah, sedikit lebih kompleks, sedangkan rebasesolusinya melakukan operasi logis yang setara satu langkah pada satu waktu.

Coba resep berikut ini:

# create and check out a temporary branch at the location of the bad merge
git checkout -b tmpfix <sha1-of-merge>

# remove the incorrectly added file
git rm somefile.orig

# commit the amended merge
git commit --amend

# go back to the master branch
git checkout master

# replant the master branch onto the corrected merge
git rebase tmpfix

# delete the temporary branch
git branch -d tmpfix

(Perhatikan bahwa Anda sebenarnya tidak memerlukan cabang sementara, Anda bisa melakukan ini dengan 'HEAD terpisah', tetapi Anda perlu mencatat id komit yang dihasilkan oleh git commit --amendlangkah untuk memasok ke git rebaseperintah daripada menggunakan cabang sementara nama.)


6
Bukankah git rebase -ilebih cepat dan masih mudah? $ git rebase -i <sh1-of-merge> Tandai yang benar sebagai "edit" $ git rm somefile.orig $ git komit - ubah $ git rebase - lanjutkan Namun untuk beberapa alasan saya masih memiliki file itu di suatu tempat yang terakhir waktu saya melakukan itu. Mungkin melewatkan sesuatu.
Wernight

12
git rebase -isangat berguna, terutama ketika Anda memiliki beberapa operasi rebase-y untuk dilakukan, tetapi itu adalah rasa sakit yang tepat untuk menggambarkan secara akurat ketika Anda tidak benar-benar menunjuk ke bahu seseorang dan dapat melihat apa yang mereka lakukan dengan editor mereka. Saya menggunakan vim, tetapi tidak semua orang akan senang dengan: "ggjcesquash <Esc> jddjp: wq" dan instruksi seperti "Pindahkan baris teratas ke setelah baris kedua saat ini dan ubah kata pertama pada baris empat menjadi 'edit' sekarang simpan dan berhenti "dengan cepat tampak lebih rumit daripada langkah sebenarnya. Anda biasanya berakhir dengan beberapa --amenddan --continuetindakan, juga.
CB Bailey

3
Saya melakukan ini tetapi komit baru diterapkan kembali di atas yang diubah, dengan pesan yang sama. Rupanya git melakukan penggabungan 3 cara antara komit lama yang tidak diubah yang mengandung file yang tidak diinginkan, dan komit tetap dari cabang lain, dan karenanya ia membuat komit baru di atas yang lama, untuk menerapkan kembali file tersebut.

6
@UncleCJ: Apakah file Anda ditambahkan dalam gabungan komit? Ini penting. Resep ini dirancang untuk mengatasi komit gabungan yang buruk. Ini tidak akan berfungsi jika file yang tidak diinginkan Anda ditambahkan dalam komit normal dalam sejarah.
CB Bailey

1
Saya kagum bagaimana saya bisa melakukan ini menggunakan smartgit dan tidak ada terminal sama sekali! Terima kasih untuk resepnya!
cregox

209

Intro: Anda Memiliki 5 Solusi

Poster asli menyatakan:

Saya tidak sengaja melakukan file yang tidak diinginkan ... ke repositori saya beberapa komit yang lalu ... Saya ingin sepenuhnya menghapus file dari riwayat repositori.

Apakah mungkin untuk menulis ulang sejarah perubahan sedemikian rupa sehingga filename.origtidak pernah ditambahkan ke repositori?

Ada banyak cara berbeda untuk menghapus riwayat file sepenuhnya dari git:

  1. Mengubah komitmen.
  2. Reset keras (mungkin ditambah rebase).
  3. Rebase non-interaktif.
  4. Rebases interaktif.
  5. Memfilter cabang.

Dalam hal poster asli, mengubah komit sebenarnya bukan pilihan dengan sendirinya, karena dia membuat beberapa komitmen tambahan setelahnya, tetapi demi kelengkapan, saya juga akan menjelaskan bagaimana melakukannya, untuk siapa pun yang hanya ingin untuk mengubah komit mereka sebelumnya.

Perhatikan bahwa semua solusi ini melibatkan mengubah / menulis ulang riwayat / komit dengan satu cara lain, sehingga siapa pun yang memiliki salinan komit lama harus melakukan pekerjaan ekstra untuk menyinkronkan kembali sejarah mereka dengan riwayat baru.


Solusi 1: Mengubah Komitmen

Jika Anda secara tidak sengaja membuat perubahan (seperti menambahkan file) di komit sebelumnya, dan Anda tidak ingin riwayat perubahan itu ada lagi, maka Anda dapat mengubah komit sebelumnya untuk menghapus file dari itu:

git rm <file>
git commit --amend --no-edit

Solusi 2: Hard Reset (Kemungkinan Plus Rebase)

Seperti solusi # 1, jika Anda hanya ingin menghilangkan komit Anda sebelumnya, maka Anda juga memiliki opsi untuk melakukan hard reset ke induknya:

git reset --hard HEAD^

Perintah tersebut akan sulit-ulang cabang Anda dengan sebelumnya 1 st orangtua komit.

Namun , jika, seperti poster asli, Anda telah membuat beberapa komit setelah komit yang ingin Anda batalkan perubahannya, Anda masih dapat menggunakan pengaturan ulang yang keras untuk memodifikasinya, tetapi melakukannya juga melibatkan penggunaan rebase. Berikut adalah langkah-langkah yang dapat Anda gunakan untuk mengubah komitmen lebih lanjut dalam sejarah:

# Create a new branch at the commit you want to amend
git checkout -b temp <commit>

# Amend the commit
git rm <file>
git commit --amend --no-edit

# Rebase your previous branch onto this new commit, starting from the old-commit
git rebase --preserve-merges --onto temp <old-commit> master

# Verify your changes
git diff master@{1}

Solusi 3: Rebase Non-interaktif

Ini akan berfungsi jika Anda hanya ingin menghapus seluruh komit dari riwayat:

# Create a new branch at the parent-commit of the commit that you want to remove
git branch temp <parent-commit>

# Rebase onto the parent-commit, starting from the commit-to-remove
git rebase --preserve-merges --onto temp <commit-to-remove> master

# Or use `-p` insteda of the longer `--preserve-merges`
git rebase -p --onto temp <commit-to-remove> master

# Verify your changes
git diff master@{1}

Solusi 4: Rebases Interaktif

Solusi ini akan memungkinkan Anda untuk mencapai hal-hal yang sama seperti solusi # 2 dan # 3, yaitu memodifikasi atau menghapus commit lebih lanjut dalam sejarah daripada komit Anda sebelumnya, sehingga solusi yang Anda pilih untuk digunakan adalah terserah Anda. Rebases interaktif tidak cocok untuk rebending ratusan komit, karena alasan kinerja, jadi saya akan menggunakan rebases non-interaktif atau solusi cabang filter (lihat di bawah) dalam situasi semacam itu.

Untuk memulai rebase interaktif, gunakan yang berikut:

git rebase --interactive <commit-to-amend-or-remove>~

# Or `-i` instead of the longer `--interactive`
git rebase -i <commit-to-amend-or-remove>~

Ini akan menyebabkan git untuk memundurkan kembali komit ke induk dari komit yang ingin Anda modifikasi atau hapus. Ini kemudian akan memberi Anda daftar komitmen rewound dalam urutan terbalik dalam editor git apa pun yang ditetapkan untuk digunakan (ini adalah Vim secara default):

pick 00ddaac Add symlinks for executables
pick 03fa071 Set `push.default` to `simple`
pick 7668f34 Modify Bash config to use Homebrew recommended PATH
pick 475593a Add global .gitignore file for OS X
pick 1b7f496 Add alias for Dr Java to Bash config (OS X)

Komit yang ingin Anda modifikasi atau hapus akan berada di bagian atas daftar ini. Untuk menghapusnya, cukup hapus barisnya dalam daftar. Jika tidak, ganti "memilih" dengan "edit" pada 1 st line, seperti begitu:

edit 00ddaac Add symlinks for executables
pick 03fa071 Set `push.default` to `simple`

Selanjutnya, masuk git rebase --continue. Jika Anda memilih untuk menghapus komit sepenuhnya, maka hanya itu yang perlu Anda lakukan (selain verifikasi, lihat langkah terakhir untuk solusi ini). Jika, di sisi lain, Anda ingin memodifikasi komit, maka git akan menerapkan kembali komit dan kemudian menjeda rebase.

Stopped at 00ddaacab0a85d9989217dd9fe9e1b317ed069ac... Add symlinks
You can amend the commit now, with

        git commit --amend

Once you are satisfied with your changes, run

        git rebase --continue

Pada titik ini, Anda dapat menghapus file dan mengubah komit, kemudian melanjutkan rebase:

git rm <file>
git commit --amend --no-edit
git rebase --continue

Itu dia. Sebagai langkah terakhir, apakah Anda memodifikasi komit atau menghapusnya sepenuhnya, itu ide yang baik untuk memverifikasi bahwa tidak ada perubahan tak terduga lainnya yang dilakukan pada cabang Anda dengan membedakannya dengan statusnya sebelum rebase:

git diff master@{1}

Solusi 5: Memfilter Cabang

Akhirnya, solusi ini yang terbaik jika Anda ingin menghapus semua jejak keberadaan file dari sejarah, dan tidak ada solusi lain yang cukup untuk tugas itu.

git filter-branch --index-filter \
'git rm --cached --ignore-unmatch <file>'

Itu akan menghapus <file>dari semua komit, mulai dari komit root. Jika sebaliknya Anda hanya ingin menulis ulang rentang komit HEAD~5..HEAD, maka Anda bisa meneruskannya sebagai argumen tambahan filter-branch, seperti yang ditunjukkan dalam jawaban ini :

git filter-branch --index-filter \
'git rm --cached --ignore-unmatch <file>' HEAD~5..HEAD

Sekali lagi, setelah filter-branchselesai, biasanya ide yang baik untuk memverifikasi bahwa tidak ada perubahan tak terduga lainnya dengan membedakan cabang Anda dengan keadaan sebelumnya sebelum operasi penyaringan:

git diff master@{1}

Alternatif Filter-Cabang: BFG Repo Cleaner

Saya pernah mendengar bahwa alat Repo Cleaner BFG berjalan lebih cepat daripada git filter-branch, jadi Anda mungkin ingin memeriksanya sebagai opsi juga. Bahkan disebutkan secara resmi dalam dokumentasi cabang-filter sebagai alternatif yang layak:

git-filter-branch memungkinkan Anda membuat penulisan ulang shell-script yang kompleks dari sejarah Git Anda, tetapi Anda mungkin tidak membutuhkan fleksibilitas ini jika Anda hanya menghapus data yang tidak diinginkan seperti file besar atau kata sandi. Untuk operasi-operasi tersebut, Anda mungkin ingin mempertimbangkan The BFG Repo-Cleaner , sebuah alternatif berbasis JVM untuk git-filter-branch, biasanya setidaknya 10-50x lebih cepat untuk case-use tersebut, dan dengan karakteristik yang sangat berbeda:

  • Versi file tertentu dibersihkan tepat sekali . BFG, tidak seperti git-filter-branch, tidak memberi Anda kesempatan untuk menangani file secara berbeda berdasarkan di mana atau kapan itu dilakukan dalam sejarah Anda. Batasan ini memberikan manfaat kinerja inti dari The BFG, dan sangat cocok untuk tugas membersihkan data buruk - Anda tidak peduli di mana data buruk itu, Anda hanya ingin itu hilang .

  • Secara default BFG mengambil keuntungan penuh dari mesin multi-core, membersihkan pohon file komit secara paralel. membersihkan git-filter-cabang komit berurutan (yaitu dengan cara tunggal berulir), meskipun adalah mungkin untuk menulis filter yang mencakup parallellism mereka sendiri, di skrip dijalankan terhadap setiap komit.

  • The opsi perintah jauh lebih ketat daripada cabang git-filter, dan didedikasikan hanya untuk tugas-tugas menghapus yang tidak diinginkan data- misalnya: --strip-blobs-bigger-than 1M.

Sumber daya tambahan

  1. Pro Git § 6.4 Alat Git - Sejarah Penulisan Ulang .
  2. git-filter-branch (1) Halaman Manual .
  3. git-commit (1) Halaman Manual .
  4. git-reset (1) Halaman Manual .
  5. git-rebase (1) Halaman Manual .
  6. Pembersih Repo BFG (lihat juga jawaban dari penciptanya sendiri ).

Apakah filter-branchmenyebabkan penghitungan ulang hash? Jika sebuah tim bekerja dengan repo di mana file besar harus disaring, bagaimana mereka melakukan ini sehingga semua orang berakhir dengan keadaan repo yang sama?
YakovL

@YakovL. Semuanya menghitung ulang hash. Sebenarnya komitmen tidak dapat diubah. Ini menciptakan sejarah yang sama sekali baru, dan memindahkan pointer cabang Anda ke sana. Satu-satunya cara untuk memastikan setiap orang memiliki riwayat yang sama adalah pengaturan ulang yang sulit.
Fisikawan Gila

118

Jika Anda belum melakukan apa pun sejak itu, cukup git rmfile dan git commit --amend.

Jika Anda memiliki

git filter-branch \
--index-filter 'git rm --cached --ignore-unmatch path/to/file/filename.orig' merge-point..HEAD

akan melalui setiap perubahan dari merge-pointmenjadi HEAD, menghapus nama file.orig dan menulis ulang perubahan. Menggunakan --ignore-unmatchberarti perintah tidak akan gagal jika karena alasan filename.orig hilang dari perubahan. Itulah cara yang disarankan dari bagian Contoh di halaman manual git-filter-branch .

Catatan untuk pengguna Windows: Jalur file harus menggunakan garis miring


3
Terima kasih! git filter-branch bekerja untuk saya di mana contoh rebase yang diberikan sebagai jawaban tidak: Langkah-langkahnya tampaknya berhasil, tetapi kemudian mendorong gagal. Menarik, lalu mendorong dengan sukses, tetapi file itu masih ada. Mencoba mengulang langkah rebase dan kemudian semuanya berantakan dengan konflik gabungan. Saya menggunakan perintah filter-branch yang sedikit berbeda, "An Improved Method" yang diberikan di sini: github.com/guides/completely-remove-a-file-from-all-revision git filter-branch -f --index- filter 'git update-index --remove filename' <introduction-revision-sha1> .. HEAD
atomicules

1
Saya tidak yakin metode mana yang ditingkatkan . Dokumentasi resmi Git git-filter-branchsepertinya memberi yang pertama.
Wernight

5
Lihat zyxware.com/articles/4027/... Saya menemukannya solusi paling lengkap dan lurus ke depan yang melibatkanfilter-branch
leontalbot

2
@atomicules, jika Anda akan mencoba untuk mendorong repo lokal ke yang jauh, git akan bersikeras menarik dari remote terlebih dahulu, karena ia memiliki perubahan yang tidak Anda miliki secara lokal. Anda dapat menggunakan - memaksa bendera untuk mendorong ke remote - itu akan menghapus file dari sana sepenuhnya. Tapi berhati-hatilah, pastikan Anda tidak akan memaksa menimpa sesuatu selain file saja.
sol0mka

1
Ingatlah untuk menggunakan "dan tidak 'saat menggunakan Windows, atau Anda akan mendapatkan kesalahan "revisi buruk" yang tidak membantu.
cz

49

Ini adalah cara terbaik:
http://github.com/guides/completely-remove-a-file-from-all-revisions

Pastikan untuk membuat cadangan salinan file terlebih dahulu.

EDIT

Hasil edit oleh Neon sayangnya ditolak saat ditinjau.
Lihat Neons pos di bawah ini, mungkin berisi informasi yang berguna!


Misalnya untuk menghapus semua *.gzfile yang secara tidak sengaja dilakukan ke repositori git:

$ du -sh .git ==> e.g. 100M
$ git filter-branch --index-filter 'git rm --cached --ignore-unmatch *.gz' HEAD
$ git push origin master --force
$ rm -rf .git/refs/original/
$ git reflog expire --expire=now --all
$ git gc --prune=now
$ git gc --aggressive --prune=now

Itu masih tidak berhasil untuk saya? (Saya saat ini di git versi 1.7.6.1)

$ du -sh .git ==> e.g. 100M

Tidak yakin mengapa, karena saya hanya punya SATU cabang master. Bagaimanapun, saya akhirnya mendapatkan repositori git saya benar-benar dibersihkan dengan mendorong ke repositori kosong dan kosong git baru, misalnya

$ git init --bare /path/to/newcleanrepo.git
$ git push /path/to/newcleanrepo.git master
$ du -sh /path/to/newcleanrepo.git ==> e.g. 5M 

(Iya!)

Lalu saya mengkloning itu ke direktori baru dan pindah ke folder .git ke yang ini. misalnya

$ mv .git ../large_dot_git
$ git clone /path/to/newcleanrepo.git ../tmpdir
$ mv ../tmpdir/.git .
$ du -sh .git ==> e.g. 5M 

(Ya! Akhirnya dibersihkan!)

Setelah memverifikasi bahwa semuanya baik-baik saja, maka Anda dapat menghapus ../large_dot_gitdan ../tmpdirdirektori (mungkin dalam beberapa minggu atau bulan dari sekarang, untuk berjaga-jaga ...)


1
Ini bekerja untuk saya sebelum "Itu masih tidak berhasil untuk saya?" komentar
shadi

Jawaban yang bagus, tetapi menyarankan --prune-emptyuntuk menambahkan ke perintah filter-branch.
ideasman42

27

Menulis ulang sejarah Git menuntut perubahan semua id komit yang terkena dampak, sehingga setiap orang yang mengerjakan proyek perlu menghapus salinan lama repo mereka, dan melakukan klon baru setelah Anda membersihkan histori. Semakin banyak orang yang mengalami ketidaknyamanan, semakin Anda membutuhkan alasan yang baik untuk melakukannya - file berlebihan Anda tidak benar-benar menyebabkan masalah, tetapi jika hanya Anda yang mengerjakan proyek, Anda mungkin juga membersihkan sejarah Git jika Anda mau untuk!

Untuk membuatnya semudah mungkin, saya sarankan menggunakan BFG Repo-Cleaner , alternatif yang lebih sederhana dan lebih cepat untuk git-filter-branchsecara khusus dirancang untuk menghapus file dari sejarah Git. Salah satu cara yang membuat hidup Anda lebih mudah di sini adalah bahwa itu sebenarnya menangani semua referensi secara default (semua tag, cabang, dll) tetapi juga 10 - 50x lebih cepat.

Anda harus hati-hati mengikuti langkah-langkah di sini: http://rtyley.github.com/bfg-repo-cleaner/#usage - tetapi inti intinya adalah ini: unduh toples BFG (memerlukan Java 6 atau lebih tinggi) dan jalankan perintah ini :

$ java -jar bfg.jar --delete-files filename.orig my-repo.git

Seluruh riwayat repositori Anda akan dipindai, dan setiap file bernama filename.orig(yang tidak ada dalam komit terbaru Anda ) akan dihapus. Ini jauh lebih mudah daripada menggunakan git-filter-branchuntuk melakukan hal yang sama!

Pengungkapan penuh: Saya penulis Repo-Cleaner BFG.


4
Ini adalah alat yang sangat baik: perintah tunggal, ini menghasilkan output yang sangat jelas dan menyediakan file log yang cocok dengan setiap komit lama dengan yang baru . Saya tidak suka menginstal Java tetapi ini sepadan.
mikemaccana

Ini adalah satu-satunya hal yang bekerja untuk saya, tetapi itu seperti karena saya tidak bekerja git-cabang filter dengan benar. :-)
Kevin LaBranche

14
You should probably clone your repository first.

Remove your file from all branches history:
git filter-branch --tree-filter 'rm -f filename.orig' -- --all

Remove your file just from the current branch:
git filter-branch --tree-filter 'rm -f filename.orig' -- --HEAD    

Lastly you should run to remove empty commits:
git filter-branch -f --prune-empty -- --all

1
Meskipun semua jawaban tampaknya ada di jalur cabang-filter, jawaban ini menyoroti cara membersihkan SEMUA cabang dalam riwayat Anda.
Cameron Lowell Palmer

4

Hanya untuk menambahkan itu ke solusi Charles Bailey, saya hanya menggunakan git rebase -i untuk menghapus file yang tidak diinginkan dari komit sebelumnya dan itu bekerja seperti pesona. Langkah langkah:

# Pick your commit with 'e'
$ git rebase -i

# Perform as many removes as necessary
$ git rm project/code/file.txt

# amend the commit
$ git commit --amend

# continue with rebase
$ git rebase --continue

4

Cara paling sederhana yang saya temukan disarankan oleh leontalbot(sebagai komentar), yang merupakan posting yang diterbitkan oleh Anoopjohn . Saya pikir nilainya ruang sendiri sebagai jawaban:

(Saya mengonversinya menjadi skrip bash)

#!/bin/bash
if [[ $1 == "" ]]; then
    echo "Usage: $0 FILE_OR_DIR [remote]";
    echo "FILE_OR_DIR: the file or directory you want to remove from history"
    echo "if 'remote' argument is set, it will also push to remote repository."
    exit;
fi
FOLDERNAME_OR_FILENAME=$1;

#The important part starts here: ------------------------

git filter-branch -f --index-filter "git rm -rf --cached --ignore-unmatch $FOLDERNAME_OR_FILENAME" -- --all
rm -rf .git/refs/original/
git reflog expire --expire=now --all
git gc --prune=now
git gc --aggressive --prune=now

if [[ $2 == "remote" ]]; then
    git push --all --force
fi
echo "Done."

Semua kredit ditujukan untuk Annopjohn, dan leontalbotuntuk menunjukkannya.

CATATAN

Ketahuilah bahwa skrip tidak menyertakan validasi, jadi pastikan Anda tidak membuat kesalahan dan bahwa Anda memiliki cadangan jika terjadi kesalahan. Itu bekerja untuk saya, tetapi mungkin tidak berhasil dalam situasi Anda. GUNAKAN DENGAN AWAS (ikuti tautan jika Anda ingin tahu apa yang sedang terjadi).


3

Jelas, git filter-branchini jalan yang harus ditempuh.

Sayangnya, ini tidak akan cukup untuk sepenuhnya menghapus filename.origdari repo Anda, karena masih dapat dirujuk oleh tag, entri reflog, remote dan sebagainya.

Saya sarankan menghapus semua referensi ini juga, dan kemudian memanggil pengumpul sampah. Anda dapat menggunakan git forget-blobskrip dari situs web ini untuk melakukan semua ini dalam satu langkah.

git forget-blob filename.orig


1

Jika ini adalah komit terbaru yang ingin Anda bersihkan, saya mencoba dengan git versi 2.14.3 (Apple Git-98):

touch empty
git init
git add empty
git commit -m init

# 92K   .git
du -hs .git

dd if=/dev/random of=./random bs=1m count=5
git add random
git commit -m mistake

# 5.1M  .git
du -hs .git

git reset --hard HEAD^
git reflog expire --expire=now --all
git gc --prune=now

# 92K   .git
du -hs .git

git reflog expire --expire=now --all; git gc --prune=nowadalah hal yang sangat buruk untuk dilakukan. Kecuali Anda kehabisan ruang disk, biarkan sampah git mengumpulkan komitmen ini setelah beberapa minggu
avmohan

Terima kasih telah menunjukkannya. Repo saya dikirim dengan banyak file biner besar dan repo didukung sepenuhnya setiap malam. Jadi saya hanya ingin setiap bagian dari itu;)
clarkttfu


-1

Anda juga bisa menggunakan:

git reset HEAD file/path


3
Jika file telah ditambahkan ke komit maka ini bahkan tidak menghapus file dari indeks, itu hanya mereset indeks ke versi file HEAD.
CB Bailey
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.