Мій офіс хоче нескінченне злиття філій як політику; які у нас є інші варіанти?


119

Мій офіс намагається з'ясувати, як ми обробляємо розбиття гілок і злиття, і ми зіткнулися з великою проблемою.

Наша проблема стосується довгострокових бічних гілок - типу, коли у вас є декілька людей, які працюють на бічній гілці, яка розпадається від майстра, ми розробляємо протягом декількох місяців, і коли ми досягнемо важливої ​​віхи, ми синхронізуємо їх.

Тепер, ІМХО, природний спосіб впоратися з цим полягає в тому, щоб розрізати бічну гілку в єдину комісію. masterпродовжує прогресувати вперед; як слід - ми не зволікаємо задніми місяцями паралельного розвитку в masterісторію Росії. І якщо комусь потрібна краща роздільна здатність для історії бічної гілки, ну, звичайно, це все ще є - просто не в master, це в бічній галузі.

Ось проблема: я працюю виключно з командним рядком, але решта моєї команди використовує GUIS. І я виявив, що в GUIS немає розумного варіанту відображення історії з інших галузей. Тож якщо ви досягнете комба-сквош, кажучи, що "цей розвиток відірвався від гілки XYZ", дуже сильно боліти подивитися, що там XYZ.

На SourceTree, наскільки я можу знайти, це величезний головний біль: якщо ви ввімкнули masterі хочете переглянути історію master+devFeature, вам потрібно перевірити master+devFeature(торкаючись кожного окремого файлу), або ще прокрутіть журнал, де паралельно відображаються ВСІ гілки вашого сховища, поки ви не знайдете потрібне місце. І удачі з’ясувати, де ти там.

Мої товариші по команді, цілком справедливо, не хочуть мати історію розвитку такою недоступною. Тому вони хочуть, щоб ці великі, довгі бічні напрямки розвитку об'єднувались завжди з об'єднанням. Вони не хочуть, щоб історія, яка не була одразу доступна з головного відділення.

Я ненавиджу цю ідею; це означає нескінченний, немислимий клубок історії паралельного розвитку. Але я не бачу, яка у нас альтернатива. І я досить збентежений; це, здається, блокує більшість всього, що я знаю про гарне управління філіями, і це буде для мене постійним розладом, якщо я не можу знайти рішення.

Чи є у нас тут якийсь варіант, окрім постійно злиття бічних гілок у головний з об'єднаннями? Або є причина, що постійно використовувати об'єднання-комміти не так вже й погано, як я боюся?


84
Невже запис злиття як злиття насправді такий поганий? Я можу бачити, що врізання в лінійну історію має свої переваги, але я здивований, що не робити цього суперечить "більшості всього, що ви знаєте про хороше управління галузями". Які саме проблеми, яких ти боїшся?
IMSoP

65
Добре, але , на мій погляд тривалий гілка , де саме ви робите хочете , щоб деякі видимість, так що звинувачує і розсікає не тільки земля , на «зміна було введено як частина Великого Rewrite 2016». Я недостатньо впевнений, щоб опублікувати як відповідь, але мій інстинкт полягає в тому, що це завдання в галузі функцій, які слід розбити, щоб у вас була коротка історія проекту, доступна з основної історії, без того, щоб перевірити осиротілу гілку.
IMSoP

5
Попри це, цілком можливо, таким чином питання зводиться до "я намагаюсь використовувати git на більш високому рівні, ніж це роблять мої товариші по команді; як я не можу їх заважати мені". Що, чесно кажучи, я, чесно кажучи, не очікував, git log master+bugfixDec10thщо стане точкою
перелому

26
"Довгострокові бічні гілки - такий, де у вас є кілька людей, які працюють на бічній гілці, яка розщеплюється від майстра, ми розробляємо протягом декількох місяців, і коли ми досягнемо важливої ​​віхи, ми синхронізуємо їх вгору." Ви періодично не тягнетеся від господаря до бічної гілки? При цьому кожен (нечисленний) зобов’язаний оволодіти, як правило, робить життя більш простим у багатьох випадках.
TafT

4
Це merge --no-ffте, що ти хочеш? У masterсебе є один комітет, який описує, що змінилося у гілці, але всі комісії все ще існують та передаються в HEAD.
CAD97

Відповіді:


244

Навіть незважаючи на те, що я використовую Git в командному рядку - я повинен погодитися з вашими колегами. Немає сенсу розкручувати великі зміни в один комітет. Ти так втрачаєш історію, а не робиш її менш помітною.

Сенс контролю джерела - відстежувати історію всіх змін. Коли що змінилося, чому? З цією метою кожна комісія містить вказівники на батьківські коміти, diff та метадані, як повідомлення про виконання. Кожна комісія описує стан вихідного коду та повну історію всіх змін, які призвели до цього стану. Збирач сміття може видаляти комісії, які недоступні.

Такі дії, як перезавантаження, вишня чи збирання зубів, видаляють або переписують історію. Зокрема, отримані комісії більше не посилаються на оригінали. Врахуйте це:

  • Ви стискаєте деякі коміти та зазначаєте у повідомленні про те, що історія, що склалася, доступна в оригінальному фіксації abcd123.
  • Ви видаляєте [1] всі гілки або теги, які включають abcd123 з моменту їх об’єднання.
  • Ви пускаєте сміттєзбірник.

[1]: Деякі сервери Git дозволяють захистити гілки від випадкового видалення, але я сумніваюся, що ви хочете зберегти всі гілки функцій на вічність.

Тепер ви більше не можете шукати цю команду - вона просто не існує.

Посилання на ім'я гілки у повідомленні про фіксацію ще гірше, оскільки назви гілок є локальними для репо. Те, що є master+devFeatureу вашій місцевій касі, може бути і doodlediduhв моєму. Гілки - це просто переміщувані мітки, які вказують на якийсь об'єкт фіксації.

З усіх методів переписування історії, повторне використання носіїв є найбільш доброякісним, оскільки воно дублює цілі коміти з усією їх історією та просто замінює батьківську команду.

Те, що masterісторія включає повну історію всіх галузей, які були злиті в неї, є хорошою справою, адже це репрезентує реальність. [2] Якщо була паралельна розробка, це повинно бути видно в журналі.

[2]: З цієї причини я також віддаю перевагу явним комісіям об'єднання над лінеаризованою, але в кінцевому підсумку фальшивою історією, отриманою в результаті повторного використання.

У командному рядку дуже git logнамагається спростити відображувану історію та зберегти всі відображені комісії. Ви можете налаштувати спрощення історії відповідно до ваших потреб. Можливо, вам сподобається написати власний інструмент журналу git, який ходить по графіку фіксації, але, як правило, неможливо відповісти "чи спочатку ця комісія була здійснена на тій чи іншій гілці?". Перший з батьків об'єкта злиття є попереднім HEAD, тобто виконуваним у галузі, в яку ви об'єднуєтесь. Але це передбачає, що ви не зробили зворотного злиття з головного в гілку функції, а потім швидкого переходу в головний злиття.

Найкраще рішення для довгострокових гілок, з якими я стикався, - це запобігання гілкам, які зливаються лише через пару місяців. Об’єднання найпростіше, коли зміни є нещодавніми і невеликими. В ідеалі ви злитесь хоча б раз на тиждень. Безперервна інтеграція (як у екстремальному програмуванні, а не як у «давайте налаштуємо сервер Дженкінса») навіть пропонує декілька об'єднань на день, тобто не підтримувати окремі гілки функцій, а ділити галузь розвитку як команду. Об’єднання перед функцією QA'd вимагає, щоб ця функція була прихована за прапором функції.

Натомість часта інтеграція дає змогу виявити потенційні проблеми набагато раніше і допомагає зберігати послідовну архітектуру: можливі далекосяжні зміни, оскільки ці зміни швидко включаються у всі галузі. Якщо зміна порушить якийсь код, це перерве лише пару днів роботи, а не пару місяців.

Переписування історії може мати сенс для справді величезних проектів, коли існує кілька мільйонів рядків коду та сотні чи тисячі активних розробників. Сумнівно, чому такий великий проект повинен був бути єдиним git repo, а не ділитись на окремі бібліотеки, але в такому масштабі зручніше, якщо центральний репо містить лише "випуски" окремих компонентів. Наприклад, в ядрі Linux використовується сквош, щоб зберегти основну історію в управлінні. Деякі проекти з відкритим кодом вимагають надсилання патчів електронною поштою замість злиття на рівні git.


43
@Standback Мені все одно, що розробник робить локально ... використовує "wip" зобов’язання, додаткові комісії для кожного фіксованого помилки,…. Це добре, краще робити занадто часто. В ідеалі розробник очищає ці зобов’язання перед тим, як вони будуть натиснуті, як поєднання всіх комітетів, які просто фіксують певні помилки. Це все ще гарна ідея зберігати комісії окремо, якщо вони роблять різні речі, наприклад, одна фіксація для фактичної функціональності та пов'язаних тестів, інша для непов’язаних помилок друку. Але після того, як комітети будуть висунуті, переписування чи видалення історії - це більше проблем, ніж це варто, принаймні з мого досвіду.
амон

11
"взагалі неможливо відповісти:" це першочергове зобов'язання було скоєне на тій чи іншій гілці? " Я дуже хочу запитати свою систему контролю версій "що було у відділенні x на дату".
Пітер Грін

80
Ніщо не засмучує те, що намагатися виявити причину помилки в коді за допомогою git bisect, тільки щоб вона отримала 10 000 рядків uber-коміт з бічної гілки.
tpg2114

2
@Standback IMHO, чим менша фіксація, тим вона читає та дружніша. Послідовність, яка стосується більше кількох точок, неможливо зрозуміти з першого погляду, тому ви просто приймаєте опис за номіналом. Це читабельність опису (наприклад, "реалізована функція X"), а не самому виконувати (код). Я вважаю за краще взяти 1000
однолітерних комітетів на

2
Cherry-pick не переписує та не видаляє історію. він просто створює нові коміти, які повторюють зміни з існуючих комітетів
Sarge Borsch

111

Мені подобається відповідь Амона , але я вважав, що одна невелика частина потребує набагато більше акценту: Ви можете легко спростити історію під час перегляду журналів, щоб задовольнити ваші потреби, але інші не можуть додавати історію під час перегляду журналів, щоб задовольнити їхні потреби. Ось чому краще зберігати історію як вона відбулася.

Ось приклад з одного з наших сховищ. Ми використовуємо модель "запит на запит", тому кожна функція виглядає як ваші довгі гілки в історії, навіть якщо вони працюють лише тиждень або менше. Індивідуальні розробники іноді вирішують збільшити історію перед об'єднанням, але ми часто поєднуємось із можливостями, тому це порівняно незвично. Ось декілька найпопулярніших gitkфайлів, що додаються до git:

стандартний перегляд gitk

Так, трохи клубка, але нам це також подобається, тому що ми можемо точно бачити, хто змінився в який час. Це точно відображає нашу історію розвитку. Якщо ми хочемо побачити подання вищого рівня, одночасно злиття запиту на потяг, ми можемо переглянути наступний вид, що еквівалентно git log --first-parentкоманді:

перегляд gitk з - перший батько

git logє ще багато варіантів, розроблених для того, щоб дати точні думки, які ви хочете. gitkможна взяти будь-який довільний git logаргумент для побудови графічного подання. Я впевнений, що інші графічні інтерфейси мають подібні можливості. Прочитайте документи та навчіться правильно ними користуватися, а не застосовувати бажаний git logпогляд на всіх під час злиття.


24
Я не був знайомий з цим варіантом! Це величезна допомога для мене, і, схоже, вивчення додаткових варіантів журналу дозволить мені простіше працювати з цим.
Резерв

25
+1 для "Ви можете легко спростити історію під час перегляду журналів, щоб задовольнити ваші потреби, але інші не можуть додавати історію під час перегляду журналів, щоб задовольнити їхні потреби". Переписування історії завжди сумнівне. Якщо ви думали, що важливо робити запис під час вчинення, то це було важливо. Навіть якщо ви виявили, що це було неправильно, або повторно зробили це пізніше, це частина історії. Деякі помилки мають сенс лише тоді, коли ви можете помітити, що цей один рядок залишився від подальшого перезапису. Коли помилки складаються з рештою епосу, ви не можете переглянути, чому все закінчилось таким, яким вони є.
TafT

@Standback Пов’язана з цим річ, яка допомагає підтримувати цю структуру для мене merge --no-ff- не використовуйте швидкі злиття вперед, натомість завжди створюйте --first-parent
комісію

34

Наша проблема стосується довгострокових бічних гілок - типу, коли у вас є декілька людей, які працюють на бічній гілці, яка розпадається від майстра, ми розробляємо протягом декількох місяців, і коли ми досягнемо важливої ​​віхи, ми синхронізуємо їх.

Перша моя думка - навіть не робити цього, якщо це абсолютно не потрібно. Ваші злиття іноді повинні бути складними. Тримайте гілки незалежними та якомога короткочасними. Це знак того, що вам потрібно розбити свої історії на менші шматки реалізації.

У випадку, якщо вам доведеться це зробити, тоді можна об'єднатись у git з опцією --no-ff, щоб історії не відрізнялися від власної гілки. Коміти все ще відображатимуться в об'єднаній історії, але їх також можна побачити окремо на гілці функцій, так що принаймні можна визначити, до якої лінії розвитку вони входили.

Я маю визнати, коли я вперше почав використовувати git, я виявив трохи дивно, що філія філій з'явилася в тій же історії, що і головна гілка після злиття. Це було трохи неприємно, тому що не здавалося, що ці історії належали до тієї історії. Але на практиці це зовсім не болісно, ​​якщо врахувати, що галузь інтеграції - це саме те, що її ціль - поєднувати гілки функцій. У нашій команді ми не проводимо сквош, і ми часто робимо об'єднання. Ми використовуємо --no-ff весь час для того, щоб легко зрозуміти точну історію будь-якої функції, якщо ми хочемо її дослідити.


3
Я повністю згоден; всі воліють триматися близько до господаря. Але це вже інше питання, яке набагато більше і набагато менше під моїм покірним впливом :-P
Резерв

2
+1 дляgit merge --no-ff
0xcaff

1
Також +1 для git merge --no-ff. Якщо ви використовуєте gitflow, це стає за замовчуванням.
Девід Хаммен

10

Дозвольте мені прямо і чітко відповісти на ваші моменти:

Наша проблема стосується довгострокових бічних гілок - типу, коли у вас є декілька людей, які працюють на бічній гілці, яка розпадається від майстра, ми розробляємо протягом декількох місяців, і коли ми досягнемо важливої ​​віхи, ми синхронізуємо їх.

Зазвичай ти не хочеш відпускати свої гілки безсистемними місяцями.

Ваша галузева функція розгалужується чимось залежно від вашого робочого процесу; давайте просто назвемо це masterзаради простоти. Тепер, коли ви берете на себе зобов’язання, ви можете і повинні git checkout long_running_feature ; git rebase master. Це означає, що ваші гілки за задумом завжди синхронізовані.

git rebaseтут також правильно робити. Це не хак чи щось дивне чи небезпечне, але абсолютно природне. Ви втрачаєте один шматочок інформації, який є "днем народження" гілки функцій, але це все. Якщо хтось вважає, що це важливо, це можна отримати, зберігаючи його десь в іншому місці (у вашій системі квитків, або, якщо потреба велика, у git tag...).

Тепер, ІМХО, природний спосіб впоратися з цим полягає в тому, щоб розрізати бічну гілку в єдину комісію.

Ні, ви абсолютно не хочете цього, ви хочете зробити об'єднання. Здійснення об'єднання також є "єдиним зобов'язанням". Це, як-небудь, не вставляє всі окремі гілки, які здійснює "в" майстер. Це єдиний вчинок з двома батьками - masterголовою та керівником відділення на момент злиття.

Обов’язково вкажіть --no-ffваріант, звичайно; злиття без --no-ff, у вашому сценарії, категорично заборонено. На жаль, --no-ffце не за замовчуванням; але я вважаю, що ви можете встановити варіант, який робить це так. Дивіться, git help mergeщо --no-ffробить (коротше: це активізує поведінку, яку я описав у попередньому пункті), це дуже важливо.

ми не заднім числом скидаємо місяці паралельного розвитку в історію майстра.

Абсолютно ні - ви ніколи не кидаєте щось "в історію" якоїсь галузі, особливо не з об'єднанням.

І якщо комусь потрібна краща роздільна здатність для історії бічної гілки, ну, звичайно, це все ще є - це просто не в майстрі, це в бічній галузі.

З об'єднанням злиття воно все ще є. Не в господарях, а в бічній гілці, добре видно як один з батьків злиття, і зберігається на вічність, як належить.

Подивіться, що я зробив? Все, що ви описуєте для вашого сквош-комітету, є саме там, де відбувається злиття --no-ff.

Ось проблема: я працюю виключно з командним рядком, але решта моєї команди використовує GUIS.

(Побічне зауваження: я майже виключно працюю і з командним рядком (ну, це брехня, я зазвичай використовую emacs magit, але це вже інша історія - якщо я не в зручному місці зі своїми індивідуальними налаштуваннями emacs, я віддаю перевагу команді лінії, а). Але, будь ласка , ласку і спробувати хоча б git guiодин раз. це так набагато більш ефективним для вибору ліній, скибки і т.д. для додавання / розстібати додає.)

І я виявив, що в GUIS немає розумного варіанту відображення історії з інших галузей.

Це тому, що те, що ви намагаєтесь зробити, повністю суперечить духу git. gitбудується з ядра на "спрямованому ациклічному графіку", що означає, що багато інформації є у ​​взаємозв'язку між батьком і дитиною комітів. А для злиття це означає, що справжнє злиття вчиняється з двома батьками та однією дитиною. GUI ваших колег буде добре, як тільки ви скористаєтеся no-ffоб'єднаннями.

Тож, якщо ви досягнете сквош-комітету, кажучи, що "цей розвиток вийшов з гілки XYZ", дуже сильно боліти подивитися, що там у XYZ.

Так, але це не проблема GUI, а компрес-сквош. Використання сквошу означає, що ви залишаєте функцію голову гілки звисати і створюєте абсолютно нову фіксацію master. Це розбиває структуру на два рівні, створюючи великий безлад.

Тому вони хочуть, щоб ці великі, довгі бічні напрямки розвитку об'єднувались завжди з об'єднанням.

І вони абсолютно праві. Але вони не «об'єднані в ", вони просто об'єднуються. Злиття - це дійсно врівноважена річ, вона не має переважної сторони, яка об'єднується "в" іншу ( git checkout A ; git merge Bточно така ж, як git checkout B ; git merge Aза винятком незначних візуальних відмінностей, як, наприклад, гілки обмінюються навколо git logтощо).

Вони не хочуть, щоб історія, яка не була одразу доступна з головного відділення.

Що цілком правильно. У той час, коли немає безперебійних функцій, у вас була б одна гілка masterз багатою історією, яка містила б у собі всі рядки фіксування функцій, які коли-небудь були, повертаючись до git initкомітету з початку часу (зауважте, що я спеціально уникав використовувати термін " гілки "в останній частині цього абзацу, оскільки історія в той час вже не є" гілками ", хоча графік фіксування був би досить галузевим).

Я ненавиджу цю ідею;

Тоді вас чекає біль, оскільки ви працюєте проти інструменту, який ви використовуєте. gitПідхід дуже елегантний і потужний, особливо в розгалуженнях / зливається області; якщо ви робите це правильно (як уже згадувалося вище, особливо --no-ffце стосується ), це скачує і перевершує інші підходи (наприклад, підривний безлад, який має паралельні структури каталогів для гілок).

це означає нескінченний, немислимий клубок історії паралельного розвитку.

Нескінченні, паралельні - так.

Немислимий, клубок - ні.

Але я не бачу, яка у нас альтернатива.

Чому б не працювати так, як це gitроблять винахідники , ваші колеги та інший світ щодня?

Чи є у нас тут якийсь варіант, окрім постійно злиття бічних гілок у головний з об'єднаннями? Або є причина, що постійно використовувати об'єднання-комміти не так вже й погано, як я боюся?

Немає інших варіантів; не так погано.


10

Розчавлення довгострокової бічної гілки змусить втратити багато інформації.

Що б я зробив, це спробувати переставити майстер на довгострокову бічну гілку, перш ніж об'єднати бічну гілку в головну . Таким чином ви зберігаєте кожну команду в майстер, роблячи історію комісій лінійною і простішою для розуміння.

Якби я не міг зробити це легко при кожному здійсненні, я б дозволю йому бути нелінійним , щоб зберегти чіткий контекст розвитку. На мою думку, якщо у мене виникне проблемне злиття під час перегляду основної частини в бічну гілку, це означає, що нелінійність мала реальне значення. Це означає, що буде легше зрозуміти, що сталося, якщо мені потрібно зануритися в історію. Я також отримую негайну вигоду від того, що не потрібно робити ребауз.


: + для згадки rebase. Незалежно від того, чи це найкращий підхід тут (я особисто не мав великого досвіду з цим, хоча я не дуже його використовував), це, безумовно, є найближчою справою до того, що дійсно хоче ОП - зокрема, компроміс між дампом історії, що не вийшов із ладу, і повністю приховує цю історію.
Кайл Странд

1

Особисто я вважаю за краще займатися розвитком, а потім тягнути запити на об'єднання в основний сховище.

Це означає, що якщо я хочу змінити свої зміни на вершині майстра або зменшити деякі WIP-зобов'язання, я можу це повністю зробити. Або я можу просто просити, щоб і моя вся історія була об'єднана.

Мені подобається займатися розвитком на гілці, але часто переглядаю програму master / dev. Таким чином я отримую останні зміни від майстра, не маючи купу злиття, що здійснює мою гілку, або не маючи справу з цілим набором конфліктів злиття, коли настав час об'єднатись з майстром.

Щоб чітко відповісти на ваше запитання:

Чи є у нас тут якийсь варіант, окрім постійно злиття бічних гілок у головний з об'єднаннями?

Так - ви можете об'єднати їх один раз за гілками (коли функція або виправлення "завершено") або якщо вам не подобається, що об'єднання об'єднань у вашій історії ви можете просто зробити швидке злиття вперед на Master після того, як виконаєте остаточну ребазу.


3
Вибачте - я роблю те саме, але не бачу, як це відповідає на питання: - /
Резерв

2
Додано чітку відповідь на ваше заявлене запитання.
Уейн Вернер

Так, це добре для моїх власних зобов'язань, але я (і моя команда) буду займатися всіма роботами. Утримувати чисту власну історію просто; робота в брудній історії розробок тут є проблемою.
Резерв

Ти маєш на увазі, що ти хочеш вимагати інших дияволів, щоб ... що? Не базуватись, а просто скошувати на злитті? Або ви просто розміщували це запитання, щоб пересвідчитися у відсутності дисципліни своїх колег у роботі?
Уейн Вернер

1
регулярно випускати оновлення не корисно, якщо в цій галузі функціонує декілька розробників.
Paŭlo Ebermann

-1

Контроль ревізії - сміття в сміття.

Проблема полягає в тому, що незавершена робота в гілці можливостей може містити багато "спробуймо це ... ні, що не вийшло, давайте замінимо його" та всі зобов'язання, окрім остаточного "того", щойно закінчиться до забруднення історії марно.

Зрештою, історію слід зберігати (частина її може бути корисною в майбутньому), але лише "чиста копія" повинна бути об'єднана.

З допомогою Git це можна зробити, спочатку розгалужуючи функціональну гілку (щоб зберегти всю історію), потім (інтерактивно) звільняючи гілку функції гілки з головного, а потім об'єднуючи відновлювану гілку.


1
Просто для уточнення: те, що ви говорите, - переписати історію на (копію) функції функції, так що у неї є коротка, чиста історія - усуваючи все назад і назад початкової розробки. А потім, злиття переписаної гілки в майстер простіше і чіткіше. Я правильно тебе зрозумів?
Резерв

1
Так. А коли ви об'єднаєтесь у головний майстер, це буде просто швидким перемогою вперед (якщо припустити, що ви перезавантажилися нещодавно перед об'єднанням).
ДепресіяДаніель

Але як зберігається ця історія? Чи дозволяєте ви оригінальній гілці функції лежати навколо?
Paŭlo Ebermann

@ PaŭloEbermann Bingo.
DepressionDaniel

Ну, як хтось ще сказав у коментарі: гілки - це лише вказівки на графік історії git, і вони локальні. Те, що названо fooв одному репо, може бути названо barв іншому. І вони покликані бути тимчасовими: ви не хочете захаращувати репо з тисячами функціональних гілок, які потрібно зберігати в живих, щоб уникнути втрати історії. І вам потрібно буде зберегти ці гілки, щоб зберегти історію довідковою, оскільки gitзгодом буде видалено будь-який комітет, який вже не доступний гілці.
cmaster
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.