Meu escritório quer filiais infinitas mescladas como política; que outras opções temos?


119

Meu escritório está tentando descobrir como lidamos com as divisões e mesclagens de filiais e encontramos um grande problema.

Nosso problema é com sidebranches de longo prazo - do tipo em que você tem algumas pessoas trabalhando em uma sidebranch que se separa do master, desenvolvemos por alguns meses e, quando atingimos um marco, sincronizamos os dois.

Agora, IMHO, a maneira natural de lidar com isso é, esmagar o sidebranch em um único commit. mastercontinua progredindo; como deveria - não estamos retroativamente lançando meses de desenvolvimento paralelo na masterhistória da empresa. E se alguém precisar de uma melhor resolução para a história do sidebranch, é claro que ainda está tudo lá - simplesmente não está presente master, está no sidebranch.

Aqui está o problema: trabalho exclusivamente com a linha de comando, mas o resto da minha equipe usa o GUIS. E descobri que o GUIS não tem uma opção razoável para exibir o histórico de outros ramos. Portanto, se você chegar a um commit de squash, dizendo "esse desenvolvimento esmagado do ramo XYZ", é uma enorme dor ver o que está acontecendo XYZ.

No SourceTree, até onde eu posso encontrar, é uma enorme dor de cabeça: se você está ligado mastere deseja ver o histórico master+devFeature, precisa dar uma olhada master+devFeature(tocando em cada arquivo diferente), ou então percorra um log exibindo TODAS as ramificações do seu repositório em paralelo até encontrar o lugar certo. E boa sorte para descobrir onde você está lá.

Meus colegas de equipe, com toda a razão, não querem ter um histórico de desenvolvimento tão inacessível. Então eles querem que essas grandes e longas sidebranches de desenvolvimento sejam mescladas, sempre com um commit de mesclagem. Eles não querem nenhum histórico que não seja imediatamente acessível a partir da ramificação principal.

Eu odeio essa ideia; significa um emaranhado infindável e inevitável da história do desenvolvimento paralelo. Mas não estou vendo que alternativa temos. E eu estou bem perplexo; isso parece bloquear quase tudo o que sei sobre um bom gerenciamento de filiais e será uma frustração constante para mim se não encontrar uma solução.

Temos alguma opção aqui além de mesclar constantemente sidebranches em master com commit de mesclagem? Ou existe uma razão pela qual o uso constante de consolidação de mesclagem não seja tão ruim quanto eu temo?


84
A gravação de uma mesclagem é realmente tão ruim assim? Percebo que a compactação em uma história linear tem suas vantagens, mas estou surpreso por não fazer isso "contra tudo o que você sabe sobre um bom gerenciamento de filiais". Quais são exatamente os problemas que você tem medo?
IMSOP

65
OK, mas na minha mente um ramo de longa duração é exatamente onde você fazer quer um pouco de visibilidade, de modo que culpa e divide não apenas pousar em "A mudança foi introduzida como parte do Big Rewrite de 2016". Não tenho confiança suficiente para postar como resposta, mas meu instinto é que são as tarefas no ramo de recursos que devem ser esmagadas, para que você tenha um breve histórico do projeto acessível a partir do histórico principal, sem precisar confira um ramo órfão.
IMSOP

5
Dito isto, é bem possível, portanto, a questão se reduz a "Estou tentando usar o git em um nível mais alto do que meus colegas de equipe; como faço para impedi-los de estragar tudo isso para mim". O que, justamente, eu honestamente não esperava git log master+bugfixDec10thser o ponto de ruptura: - /
Standback

26
"sidebranches de longo prazo - do tipo em que você tem algumas pessoas trabalhando em um sidebranch que se separa do master, desenvolvemos por alguns meses e, quando atingimos um marco, sincronizamos os dois". Você não puxa periodicamente do mestre para o ramo lateral? Fazer isso todos (poucos) se compromete a dominar tende a tornar a vida mais simples em muitos casos.
Taft

4
É o merge --no-ffque você quer? Por mastersi só, você tem um commit que descreve o que mudou no branch, mas todos os commits ainda existem e são pais de HEAD.
quer

Respostas:


243

Mesmo usando o Git na linha de comando - tenho que concordar com seus colegas. Não é sensato compactar grandes alterações em um único commit. Você está perdendo a história dessa maneira, não apenas tornando-a menos visível.

O objetivo do controle de origem é rastrear o histórico de todas as alterações. Quando o que mudou por quê? Para esse fim, cada confirmação contém ponteiros para confirmações pai, um diff e metadados como uma mensagem de confirmação. Cada confirmação descreve o estado do código-fonte e o histórico completo de todas as alterações que levaram a esse estado. O coletor de lixo pode excluir confirmações inacessíveis.

Ações como rebasing, cherry picking ou squashing excluem ou reescrevem o histórico. Em particular, as confirmações resultantes não fazem mais referência às confirmações originais. Considere isto:

  • Você esmaga algumas confirmações e observe na mensagem de confirmação que o histórico compactado está disponível na confirmação original abcd123.
  • Você exclui [1] todos os ramos ou tags que incluem abcd123 desde que eles são mesclados.
  • Você deixa o coletor de lixo funcionar.

[1]: Alguns servidores Git permitem que as filiais sejam protegidas contra exclusão acidental, mas duvido que você queira manter todas as suas ramificações de recursos por toda a eternidade.

Agora você não pode mais procurar esse commit - ele simplesmente não existe.

A referência a um nome de filial em uma mensagem de confirmação é ainda pior, pois os nomes das ramificações são locais para um repo. O que está master+devFeatureno seu checkout local pode estar doodlediduhno meu. As ramificações estão apenas movendo rótulos que apontam para algum objeto de confirmação.

De todas as técnicas de reescrita de histórico, o rebasing é o mais benigno, porque duplica as confirmações completas com todo o histórico e apenas substitui uma confirmação pai.

Que a masterhistória inclua a história completa de todos os ramos que foram fundidos nela é uma coisa boa, porque isso representa a realidade. [2] Se houve desenvolvimento paralelo, isso deve estar visível no log.

[2]: Por esse motivo, também prefiro confirmações de mesclagem explícita do que o histórico linearizado, mas, em última instância, falso, resultante do rebaseamento.

Na linha de comando, git logtenta simplificar o histórico exibido e manter todos os commits exibidos relevantes. Você pode ajustar a simplificação do histórico para atender às suas necessidades. Você pode ficar tentado a escrever sua própria ferramenta de log git que percorre o gráfico de confirmação, mas geralmente é impossível responder "esse commit foi originalmente comprometido neste ou naquele ramo?". O primeiro pai de uma consolidação de mesclagem é o anterior HEAD, ou seja, a consolidação no ramo no qual você está mesclando. Mas isso pressupõe que você não fez uma mesclagem reversa do mestre na ramificação do recurso e depois avançou o mestre rapidamente para a mesclagem.

A melhor solução para ramificações de longo prazo que encontrei é impedir ramificações que são mescladas somente após alguns meses. A fusão é mais fácil quando as alterações são recentes e pequenas. Idealmente, você mesclará pelo menos uma vez por semana. A integração contínua (como na Extreme Programming, não como em “vamos configurar um servidor Jenkins”) até sugere várias mesclagens por dia, ou seja, para não manter ramificações de recursos separadas, mas compartilhar uma ramificação de desenvolvimento como uma equipe. A mesclagem antes do controle de qualidade de um recurso exige que o recurso fique oculto atrás de um sinalizador de recurso.

Em troca, a integração frequente torna possível detectar problemas em potencial muito mais cedo e ajuda a manter uma arquitetura consistente: alterações de longo alcance são possíveis porque essas alterações são rapidamente incluídas em todas as ramificações. Se uma alteração quebrar algum código, ela quebrará apenas alguns dias de trabalho, e não alguns meses.

A reescrita do histórico pode fazer sentido para projetos realmente grandes quando existem vários milhões de linhas de código e centenas ou milhares de desenvolvedores ativos. É questionável por que um projeto tão grande teria que ser um único repositório git em vez de ser dividido em bibliotecas separadas, mas nessa escala é mais conveniente se o repositório central contiver apenas "liberações" dos componentes individuais. Por exemplo, o kernel do Linux emprega squash para manter o histórico principal gerenciável. Alguns projetos de código aberto exigem que os patches sejam enviados por email, em vez de uma mesclagem no nível git.


43
@Standback Eu não ligo para o que um desenvolvedor faz localmente ... use confirmações “wip”, confirmações extras para cada erro de digitação fixo…. Tudo bem, é melhor cometer com muita frequência. Idealmente, o desenvolvedor limpa esses commits antes de serem enviados, como combinar todos os commits que apenas corrigem alguns erros de digitação. Ainda é uma boa idéia manter os commits separados se eles fizerem coisas diferentes, por exemplo, um commit para funcionalidade real e testes relacionados, outro para erros não relacionados. Mas uma vez que as confirmações são enviadas, reescrever ou excluir o histórico é mais problemático do que vale a pena, pelo menos na minha experiência.
amon

11
“geralmente é impossível responder“ esse commit foi originalmente comprometido com esse ou aquele branch? ”“ Eu vejo o fato de que “branches” são apenas indicadores de commit sem histórico real próprio como uma das maiores falhas de design do git Eu realmente quero ser capaz de perguntar ao meu sistema de controle de versão "o que estava na ramificação x na data y".
Peter Green

80
Nada é mais frustrante do que tentar identificar a causa de um bug no código git bisect, apenas para que ele chegue a um uber-commit de 10.000 linhas a partir de um ramo lateral.
tpg2114

2
@ Standback IMHO quanto menor o commit, mais legível e amigável ele é. É impossível entender um commit que aborda mais do que alguns pontos à primeira vista; portanto, basta pegar a descrição pelo valor nominal. Isso é legibilidade da descrição (por exemplo, "recurso X implementado"), não do commit (código) em si. Eu prefiro ter 1000 confirmações de uma letra como "alterado http para https"
:)

2
cherry-pick não reescreve ou exclui o histórico. ele apenas cria novos commits que replicam as alterações dos commits existentes
Sarge Borsch

110

Gosto da resposta de Amon , mas senti que uma pequena parte precisava de muito mais ênfase: você pode simplificar facilmente o histórico enquanto visualiza os logs para atender às suas necessidades, mas outros não podem adicionar histórico enquanto visualiza os logs para atender às suas necessidades. É por isso que é preferível manter a história como ocorreu.

Aqui está um exemplo de um de nossos repositórios. Usamos um modelo de solicitação de recebimento, para que cada recurso se pareça com suas ramificações de longa duração no histórico, mesmo que normalmente executem apenas uma semana ou menos. Às vezes, desenvolvedores individuais optam por esmagar seu histórico antes de mesclar, mas geralmente combinamos recursos, o que é relativamente incomum. Aqui estão os poucos commits gitk, o gui que vem junto com o git:

visualização gitk padrão

Sim, um pouco de confusão, mas também gostamos porque podemos ver exatamente quem teve o que muda a que horas. Reflete com precisão nossa história de desenvolvimento. Se queremos ver uma visualização de nível superior, uma solicitação pull é mesclada por vez, podemos observar a seguinte visualização, que é equivalente ao git log --first-parentcomando:

visualização gitk com --first-parent

git logtem muito mais opções projetadas para fornecer exatamente as visualizações que você deseja. gitkpode usar qualquer git logargumento arbitrário para criar uma visualização gráfica. Tenho certeza que outras GUIs têm recursos semelhantes. Leia os documentos e aprenda a usá-lo corretamente, em vez de aplicar sua git logvisão preferida a todos no momento da mesclagem.


24
Eu não estava familiarizado com esta opção! Isso é uma grande ajuda para mim, e parece que aprender mais opções de log me permitirá viver com isso mais facilmente.
Standback

25
+1 para "Você pode simplificar facilmente o histórico enquanto visualiza logs para atender às suas necessidades, mas outros não podem adicionar histórico enquanto visualiza logs para atender às necessidades deles". Reescrever a história é sempre questionável. Se você achou importante gravar no momento do commit, isso era importante. Mesmo que você tenha achado errado ou refizido depois, isso faz parte da história. Alguns erros só fazem sentido quando você pode ver que a única linha foi remanescente de uma reescrita posterior. Quando os erros são repetidos com o resto do épico, você não pode avaliar por que as coisas acabaram como estão.
TafT

@Standback relacionada, uma coisa que ajuda a manter esta estrutura, para mim, está usando merge --no-ff- não use fusões fast-forward, em vez sempre criar uma mala comprometer de modo que --first-parenttem algo para trabalhar com
Izkata

34

Nosso problema é com sidebranches de longo prazo - do tipo em que você tem algumas pessoas trabalhando em uma sidebranch que se separa do master, desenvolvemos por alguns meses e, quando atingimos um marco, sincronizamos os dois.

Meu primeiro pensamento é - nem faça isso, a menos que seja absolutamente necessário. Suas fusões devem ser um desafio às vezes. Mantenha os galhos independentes e com a menor duração possível. É um sinal de que você precisa dividir suas histórias em pedaços de implementação menores.

Caso você precise fazer isso, é possível mesclar no git com a opção --no-ff para que os históricos sejam mantidos distintos em seu próprio ramo. As confirmações ainda aparecerão no histórico mesclado, mas também podem ser vistas separadamente na ramificação do recurso, para que pelo menos seja possível determinar em qual linha de desenvolvimento eles fizeram parte.

Eu tenho que admitir que quando comecei a usar o git, achei um pouco estranho que o commit do ramo aparecesse no mesmo histórico que o branch principal após a mesclagem. Foi um pouco desconcertante porque não parecia que esses commits pertenciam a essa história. Mas, na prática, não é algo realmente doloroso, se considerarmos que o ramo de integração é exatamente isso - todo o seu objetivo é combinar os ramos do recurso. Em nossa equipe, não fazemos squash e fazemos commits de mesclagem freqüentes. Usamos --no-ff o tempo todo para garantir que seja fácil ver o histórico exato de qualquer recurso, caso desejemos investigá-lo.


3
Estou de pleno acordo; todo mundo prefere ficar perto do mestre. Mas essa é uma questão diferente, que é muito maior e muito menos sob minha própria humilde influência :-P
Standback

2
+1 paragit merge --no-ff
0xcaff

1
Também +1 para git merge --no-ff. Se você usar gitflow, isso se tornará o padrão.
precisa saber é o seguinte

10

Deixe-me responder seus pontos direta e claramente:

Nosso problema é com sidebranches de longo prazo - do tipo em que você tem algumas pessoas trabalhando em uma sidebranch que se separa do master, desenvolvemos por alguns meses e, quando atingimos um marco, sincronizamos os dois.

Você geralmente não deseja deixar suas filiais sem sincronização por meses.

Sua ramificação de recurso ramificou-se de algo, dependendo do seu fluxo de trabalho; vamos chamá-lo masterpor uma questão de simplicidade. Agora, sempre que você se comprometer com o domínio, você pode e deve git checkout long_running_feature ; git rebase master. Isso significa que suas ramificações estão, por design, sempre sincronizadas.

git rebasetambém é a coisa correta a fazer aqui. Não é um truque ou algo estranho ou perigoso, mas completamente natural. Você perde um pouco de informação, que é o "aniversário" do ramo de recursos, mas é isso. Se alguém achar que isso é importante, ele poderá ser fornecido salvando-o em outro lugar (no seu sistema de tickets ou, se a necessidade for grande, em um git tag...).

Agora, IMHO, a maneira natural de lidar com isso é, esmagar o sidebranch em um único commit.

Não, você absolutamente não quer isso, deseja um commit de mesclagem. Uma consolidação de mesclagem também é uma "consolidação única". De alguma forma, ele não insere todas as confirmações individuais da ramificação "no" mestre. É um único commit com dois pais - a mastercabeça e a cabeça do ramo no momento da mesclagem.

Certifique-se de especificar a --no-ffopção, é claro; mesclar sem --no-ffdeve, no seu cenário, ser estritamente proibido. Infelizmente, --no-ffnão é o padrão; mas acredito que há uma opção que você pode definir que o faça. Veja o git help mergeque --no-fffaz (em resumo: ele ativa o comportamento que descrevi no parágrafo anterior), é crucial.

não estamos retroativamente lançando meses de desenvolvimento paralelo na história do mestre.

Absolutamente não - você nunca está despejando algo "na história" de algum ramo, especialmente não com um commit de mesclagem.

E se alguém precisar de uma melhor resolução para a história do sidebranch, bem, é claro que ainda está tudo lá - simplesmente não está no mestre, está no sidebranch.

Com um commit de mesclagem, ele ainda está lá. Não no mestre, mas no sidebranch, claramente visível como um dos pais da fusão, e mantida por toda a eternidade, como deveria ser.

Viu o que eu fiz? Todas as coisas que você descreve para o seu commit de squash estão ali com o --no-ffcommit de mesclagem .

Aqui está o problema: trabalho exclusivamente com a linha de comando, mas o resto da minha equipe usa o GUIS.

(Observação: também trabalho quase exclusivamente com a linha de comando (bem, isso é mentira, eu geralmente uso o emacs magit, mas isso é outra história - se eu não estiver em um local conveniente com a configuração individual do emacs, prefiro o comando linha também). Mas, por favor, faça um favor e tentar pelo menos git guiuma vez. é por isso muito mais eficiente para escolher linhas, pedaços etc. para adicionar / ruína completa.)

E descobri que os GUIS não têm uma opção razoável para exibir o histórico de outros ramos.

Isso porque o que você está tentando fazer é totalmente contra o espírito de git. gitconstrói a partir do núcleo em um "gráfico acíclico direcionado", o que significa que muitas informações estão no relacionamento pai-filho de confirmações. E, para mesclagens, isso significa que a verdadeira mesclagem se compromete com dois pais e um filho. As GUIs de seus colegas ficarão bem assim que você usar os no-ffcommits de mesclagem.

Portanto, se você chegar a um commit de squash, dizendo "esse desenvolvimento compactado do ramo XYZ", é uma grande dor ver o que há em XYZ.

Sim, mas isso não é um problema da GUI, mas do squash commit. Usar uma abóbora significa deixar a cabeça do ramo do recurso pendente e criar uma confirmação totalmente nova master. Isso quebra a estrutura em dois níveis, criando uma grande bagunça.

Então eles querem que essas grandes e longas sidebranches de desenvolvimento sejam mescladas, sempre com um commit de mesclagem.

E eles estão absolutamente certos. Mas eles não são "fundidos em ", eles estão apenas se fundiram. Uma mesclagem é uma coisa verdadeiramente equilibrada, não possui um lado preferido que é mesclado "no" outro ( git checkout A ; git merge Bé exatamente o mesmo que git checkout B ; git merge Aexceto por pequenas diferenças visuais, como as ramificações sendo trocadas git logetc.).

Eles não querem nenhum histórico que não seja imediatamente acessível a partir da ramificação principal.

O que é completamente correto. No momento em que não há recursos unificados, você teria um único ramo mastercom um histórico rico que encapsulava todas as linhas de confirmação de recursos que existiam, voltando ao git initcommit desde o início dos tempos (observe que eu especificamente evitei usar o termo " branches "na parte final desse parágrafo porque o histórico naquele momento não é mais" branches ", embora o gráfico de confirmação seja bastante ramificado).

Eu odeio essa ideia;

Então você sente um pouco de dor, já que está trabalhando contra a ferramenta que está usando. A gitabordagem é muito elegante e poderosa, especialmente na área de ramificação / fusão; se você fizer o que é correto (como mencionado acima, especialmente com --no-ff), é aos trancos e barrancos superado por outras abordagens (por exemplo, a confusão do subversion de ter estruturas de diretórios paralelas para ramificações).

significa um emaranhado infindável e inevitável da história do desenvolvimento paralelo.

Sem fim, paralelo - sim.

Inavegável, emaranhado - não.

Mas não estou vendo que alternativa temos.

Por que não trabalhar como o inventor git, seus colegas e o resto do mundo, todos os dias?

Temos alguma opção aqui além de mesclar constantemente sidebranches em master com commit de mesclagem? Ou existe uma razão pela qual o uso constante de consolidação de mesclagem não seja tão ruim quanto eu temo?

Nenhuma outra opção; não é tão ruim.


10

O esmagamento de uma sidebranch de longo prazo faria você perder muitas informações.

O que eu faria é tentar refazer o master no sidebranch de longo prazo antes de fundi-lo no master . Dessa forma, você mantém todos os commit no master, enquanto torna o histórico do commit linear e mais fácil de entender.

Se eu não pudesse fazer isso facilmente em cada commit, deixaria não linear , para manter o contexto de desenvolvimento claro. Na minha opinião, se eu tiver uma mescla problemática durante a reformulação do master na sidebranche, isso significa que a não linearidade tinha significado no mundo real. Isso significa que será mais fácil entender o que aconteceu, caso eu precise investigar a história. Eu também recebo o benefício imediato de não ter que fazer uma rebase.


: + por mencionar rebase. Se é a melhor abordagem aqui (eu pessoalmente não tive grandes experiências com ela, embora não a tenha usado muito), definitivamente parece ser a coisa mais próxima do que o OP realmente parece querer - especificamente, um compromisso entre um despejo de histórico fora de ordem e ocultar completamente esse histórico.
Kyle Strand

1

Pessoalmente, prefiro fazer meu desenvolvimento em um fork, depois puxar solicitações para mesclar no repositório primário.

Isso significa que, se eu quiser refazer minhas alterações sobre o master ou esmagar algumas confirmações WIP, eu posso fazer isso totalmente. Ou posso apenas solicitar que toda a minha história seja mesclada também.

O que eu gosto de fazer é fazer meu desenvolvimento em uma ramificação, mas frequentemente me refiro ao master / dev. Dessa forma, recebo as alterações mais recentes do mestre sem ter um monte de confirmações de mesclagem em minha ramificação, ou sem precisar lidar com toda uma carga de conflitos de mesclagem quando é hora de mesclar com o master.

Para responder explicitamente à sua pergunta:

Temos alguma opção aqui além de mesclar constantemente sidebranches em master com commit de mesclagem?

Sim - você pode mesclá-los uma vez por ramificação (quando o recurso ou correção estiver "completo") ou, se não gostar da consolidação da mesclagem em seu histórico, basta fazer uma mesclagem de avanço rápido no master após uma nova refazer final.


3
Sinto muito - eu faço a mesma coisa, mas não vejo como isso responde à pergunta: - /
Standby

2
Foi adicionada uma resposta explícita à sua pergunta declarada.
Wayne Werner

Então, isso é bom para meus próprios compromissos, mas eu (e minha equipe) vamos lidar com o trabalho de todos . Manter minha própria história limpa é fácil; trabalhar em uma história confusa de desenvolvedores é o problema aqui.
Standback

Quer dizer que deseja exigir outros desenvolvedores para ... o quê? Não rebase e apenas squash na mesclagem? Ou você estava apenas postando esta pergunta para reclamar sobre a falta de disciplina de seus colegas de trabalho?
Wayne Werner

1
rebasear regularmente não será útil se vários desenvolvedores estiverem trabalhando nesse ramo de recursos.
Paŭlo Ebermann

-1

O controle de revisão é entrada e saída de lixo.

O problema é que o trabalho em andamento no ramo de recursos pode conter muitos "vamos tentar isso ... não que não funcionou, vamos substituí-lo por isso" e todos os commits, exceto o final "that" acabam poluindo inutilmente a história.

Por fim, o histórico deve ser mantido (alguns poderão ser úteis no futuro), mas apenas uma "cópia limpa" deve ser mesclada.

Com o Git, isso pode ser feito ramificando a ramificação do recurso primeiro (para manter todo o histórico), depois (interativamente) reorganizando a ramificação da ramificação do recurso do mestre e mesclando a ramificação reestruturada.


1
Apenas para esclarecer: o que você está dizendo é reescrever o histórico (uma cópia) do ramo de recursos, para que ele tenha um histórico curto e limpo - eliminando todas as idas e vindas do desenvolvimento inicial. E então, mesclar a ramificação reescrita no master é mais simples e mais limpo. Eu entendi você corretamente?
Standback

1
Sim. E quando você entrar no master, será apenas um avanço rápido (supondo que você tenha se recuperado recentemente antes de mesclar).
DepressedDaniel

Mas como essa história é mantida? Você deixa o recurso original se ramificar?
Paŭlo Ebermann

@ PaŭloEbermann Bingo.
DepressedDaniel

Bem, como alguém disse em um comentário: os ramos são apenas indicadores do gráfico histórico gite são locais. O que é nomeado fooem um repositório pode ser nomeado barem outro. E eles devem ser temporários: você não deseja desorganizar seu repositório com milhares de ramos de recursos que precisam ser mantidos vivos para evitar perder o histórico. E você precisaria manter essas ramificações para manter o histórico como referência, pois git, eventualmente, excluirá qualquer confirmação que não seja mais alcançável por uma ramificação.
25/16 /
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.