[SVN] Branching strategieën

Pagina: 1
Acties:
  • 175 views sinds 30-01-2008
  • Reageer

  • ^Mo^
  • Registratie: Januari 2001
  • Laatst online: 04-11 22:31
Hoi,

Ten eerste, ik heb geen idee of mijn topic hier hoort, maar omdat het toch wel een beetje de theoretische kant is van software engineering dacht ik dat dit forum goed was... bij voorbaat excuses mocht het niet goed zijn :)

Goed, mijn vraag dan. Volgende week gaan wij op kantoor een overstap maken van CVS naar SubVersion (SVN). Tergelijkertijd willen wij onze branching strategie daarbij aanpassen, aangezien het nu te vaak op chaos uitloopt en we vaak niet meer oude versies kunnen bouwen (eerlijk gezegd doen we niet heel veel aan branchen in ons hoofdproduct).

Onze werkwijze:
Momenteel is het zo dat wij in principe elke vrijdag aan bugfixing doen in onze software (hoewel vaak ook wel andere dagen) en vrijdag ook elke keer een build klaar zetten voor QA. Deze build kan, mits goedgekeurd door QA, uitgeleverd worden (meestal de vrijdag daarop) hoewel dat op dat moment nog niet vaststaat.

Nu heb ik uit het boek Essential CVS een tweetal branching strategieën gehaald:


Basically stable



The basically stable branching philosophy states that the trunk should contain project data that is always close to being ready for release. Branches are used for development, bug fixes, prerelease QA (quality assurance), and refactoring. Branches are also used for experimental code.

The strictest variation of this philosophy states that nothing should be merged to the trunk until it has been through QA. This ensures that at any time a release candidate can be taken from the trunk, put through a small amount of QA, and then published or sold.

More lenient variations of this philosophy allow anything that passes developer unit-testing to be merged into the trunk. Such a relaxed approach requires a release candidate to be branched off and put through a full QA analysis before publication.
Advantages of the basically stable method include the following:
  • You can take a release off the trunk at any time, run it through QA, and publish it.
  • Because the trunk contains stable code that changes slowly, you can perform experimental work on a branch with little likelihood of causing errors in someone else's work when the branch is merged back into the trunk.
The greatest disadvantage of the basically stable philosophy is that merging usually gets done by a QA tester rather than by a person who understands the semantics of the code being merged. Also, the trunk may change significantly between the original time a branch was split off from it and the time the branch is merged back. Both of these problems can be reduced by having the developer merge the trunk to the branch periodically, or by using a less strict variation.


Basically unstable



The basically unstable philosophy states that the trunk should contain the latest code, regardless of its stability, and that release candidates should be branched off for QA.

The strictest variation states that all development takes place on the trunk and branches are used only for release candidates, bugfix branches, and releases.

More lenient variations also allow branching for experimental code, refactoring, and other special-case code. Merging of a branch back into the trunk is done by the managers of the branch.

The advantage of this philosophy is that merging doesn't happen often and is easier to do because it is usually done by people familiar with the code.

The disadvantage of this philosophy, especially when applied in its strictest form, is that the main trunk often contains buggy code, experimental work, and sometimes code that doesn't compile at all. Frequent tagging can reduce this disadvantage, so tag every time there's good code or whenever someone starts to experiment or refactor. The more lenient variations keep the buggiest code and the code most likely to cause significant breakage off the trunk, reducing the time it takes to prepare a release for publication.

Beide methoden spreken me aan. Maar ik denk dat de eerste strategie (basically stable) voor ons niet haalbaar is, maar wellicht dat ik het mis heb. Zoals ik het zie gaat het als volgt:
1) Developer moet vrijdag bug fixing doen, en maakt van de trunk een branch (b1).
2) Bug fixes worden in die branch gedaan
3) Build wordt klaargezet voor QA
Na stap 3 is de programmeur klaar voor deze build, en dus branch b1. Echter, de code mag in principe niet worden gemerged naar de stable trunk want de software is nog niet goedgekeurd. Echter de developer moet maandag weer verder, maar mag in principe niet meer aan die branch komen, want die is potentieel stable. Dan kan je eventueel een branch maken vanaf b1, maar ik heb het gevoel dat je dan weer het doel voorbij schiet. Wat we ook kunnen doen is op maandag een nieuwe branch maken vanaf de trunk, en de wijzigingen van vrijdag daar weer in mergen (of juist wachten totdat deze wijzigingen stable zijn, maar dan zit je dus te ontwikkelen in een branch waar bug fixes niet in zitten, lijkt me ietwat te foutgevoelig)
Het idee van een branch maken van de stabiele software bevalt me echter wel, vooral als we ingrijpende wijzigingen moeten gaan doen.

Nog een alternatief is om gelijk van branch b1 terug te mergen naar de trunk, met de gedachte dat de vorige versie (die wel stabiel was) een aparte branch is.

De basically unstable strategie is wellicht wat meer toegesneden op onze manier van werken. Je werkt in de trunk, en elke keer als er een build voor QA wordt gemaakt wordt er ook een branch gemaakt. Het nadeel hier van is dat je niet zo heel makkelijk ingrijpende wijzigingen kan doen...

Kortom, ik hink een beetje op twee gedachten, en wellicht is mijn gedachtengang niet juist. Mijn voorkeur gaat uit naar basically stable, maar dan moet er iets veranderen aan de werkwijze (met SVN dan).

Of zijn er nog alternatieve werkwijzen te bedenken?

[ Voor 2% gewijzigd door ^Mo^ op 11-11-2006 19:56 . Reden: nog een alternatief toegevoegd ]

"There are 10 kinds of people in the world, those who understand binary and those who don't" | Werkbak specs


  • JeroenB
  • Registratie: November 1999
  • Laatst online: 14-11 22:30
De manier die het beste is voor je organisatie hangt in de praktijk vooral af van hoe de samenstelling en samenwerking is. Wat ik daarmee bedoel is: als er veel andere groepen (bijv. een projectgroep die het produkt wil gebruiken) zijn die regelmatig het nieuwste van het nieuwste in een bruikbare vorm willen kunnen pakken, dan is het het handigst om uit te gaan van een trunk die stabiel is.

Als er alleen extern gebruikers zitten, dan is het geen enkel probleem om met een instabiele trunk te werken, aangezien dat de overhead vermindert tijdens de ontwikkeling zelf.

Zelf vind ik het ook altijd een lastige discussie en werk ik daarom met verschillende strategieen: projecten die veelal niet door anderen worden gebruikt ontwikkel ik puur op de trunk, waarvan dan branches worden gemaakt als release. Op die release-branches kunnen dan versie-specifieke bugfixes worden doorgevoerd. Bij libraries of componenten die ook door anderen worden gebruikt is het vaak zinvol om de trunk stabiel te houden en alle development in branches te doen: zodra een feature dan stabiel is en door alle unit-tests komt, dan wordt deze door een zgn. release-engineer (de enige developer in dat project die schrijfrechten in de trunk heeft) naar de trunk gemerged.

Let erop dat als je voor de laatste strategie gaat, het vrijwel essentieel is om die schrijfrechten in de trunk te beperken, want de gemiddelde programmeur vindt het toch altijd moeilijk om van de trunk af te blijven (ervaring).

  • H!GHGuY
  • Registratie: December 2002
  • Niet online

H!GHGuY

Try and take over the world...

Bij ons gebeurt het nog anders...

Stel dat je roadmap er als volgt uitziet:
Januari 2006: v1.0
April 2006: v1.2
Augustus 2006: v1.4

Dan wordt er nu gedeveloped op de trunk aan de features voor 1.0
Wanneer er 1.2 features geimplementeerd beginnen worden wordt een 1.0 branch gemaakt.
Die 1.0 branch wordt uiteindelijk stable, getest en de deur uitgezonden. Het gebeurt ook wel dat er merges van de 1.0 naar de 1.2 (trunk) gedaan worden. Nadien wordt 1.0 naar 1.2 gemerged en wordt daar verder gedev'd. Wanneer ook 1.4 features klaar zijn om te beginnen wordt een 1.2 branch gemaakt. enzovoort.

Het gebeurt natuurlijk ook soms dat risicovollere deelprojecten een aparte branch krijgen.
Het komt er dus eigenlijk op neer dat de trunk altijd de laatste unstable is. En dat branches leiden tot stable versies. Dit is eigenlijk wel op CVS, maar dat is eigenlijk maar een miniem detail.

ASSUME makes an ASS out of U and ME


  • ^Mo^
  • Registratie: Januari 2001
  • Laatst online: 04-11 22:31
Hmmm, lastig. Ik had een voorkeur voor een stabiele trunk, maar nu weet ik het niet meer zo zeker. Het lijkt erop dat een unstable trunk ietwat beter aansluit op onze manier van werken. Maar stel nou we gaan op die manier werken, dan heb ik nog een andere kwestie. Er zijn een aantal lange termijn features/bug fixes aan de orde, en dat betekent dat we potentieel een flink aantal weken geen (stabiele) software kunnen bouwen van de trunk. Maar er zullen ongetwijfeld bug fixes moeten worden gedaan op de uitgeleverde software. Maak je dan weer een branch vanaf die laatste stabiele build?

Voorbeeld:
Main ----------------------------------------------------------------------------
          |                     |                         |
          build 1               build 2                   build 3
                                                          |
                                                          build 4
                                                          |
                                                          build 5

"There are 10 kinds of people in the world, those who understand binary and those who don't" | Werkbak specs


  • JeroenB
  • Registratie: November 1999
  • Laatst online: 14-11 22:30
In principe maak je een tag van alles dat je uitlevert. En als dan blijkt dat je op een uitgeleverde versie fixes moet doen, dan maak je van die tag een branch en doe je daar je fixes in. Dan heb je dus altijd toegang tot:

1. De tag, die bevat wat je oorspronkelijk hebt uitgeleverd.
2. De branch, die bevat de meest recente update van die versie.
3. De trunk, die bevat de huidige development-omgeving (al dan niet stabiel, afhankelijk van de keuze die je op dat gebied hebt gemaakt).

Je kunt dan voor elke versie van een branch die naar buiten gaat opnieuw een tag maken. Zodat je in de /tags-directory direct toegang hebt tot alles dat ooit is uitgeleverd en in de /branches-directory toegang hebt tot de meest recente versie van alle releases (als er geen branch van een versie is, dan zijn er geen updates geweest sinds de tag).

[ Voor 6% gewijzigd door JeroenB op 12-11-2006 14:13 ]


  • ^Mo^
  • Registratie: Januari 2001
  • Laatst online: 04-11 22:31
Okay, dan heb ik denk ik wel een aardig beeld over hoe te gaan werken. Ik ga het eens uitwerken, en aan mijn collega's voorleggen. Bedankt voor de input :)

"There are 10 kinds of people in the world, those who understand binary and those who don't" | Werkbak specs


  • NetForce1
  • Registratie: November 2001
  • Laatst online: 14:03

NetForce1

(inspiratie == 0) -> true

Wij werken met een mix van beide strategien. De trunk bevat in princiepe niet stabiele code, deze code moet echter wel compileren anders kan de buildstraat niet testen. Als er een ingrijpende wijziging gedaan moet worden (grote refactoring oid) dan wordt daar een aparte branch voor gestart zodat de rest van de developers er geen last van heeft.

De wereld ligt aan je voeten. Je moet alleen diep genoeg willen bukken...
"Wie geen fouten maakt maakt meestal niets!"


  • djc
  • Registratie: December 2001
  • Laatst online: 08-09 23:18

djc

Ik zie veel Subversion-based projecten die een minder-stable trunk hebben waar alles in beginsel op ontwikkeld wordt, met daarnaast branches voor het stabiliseren van releases enzo. In mijn ervaring werkt dit echter het best als de trunk wel enigszins stable gehouden kan worden. Voor het echt disruptieve werk worden dan meestal wel branches gemaakt. Bedenk echter dat het parallel ontwikkelen van branches vaak wel redelijk wat onderhoud vergt (in termen van het mergen van wijzigingen van trunk naar de branch voor zolang als de branch apart wordt gehouden).

Rustacean


  • JeroenB
  • Registratie: November 1999
  • Laatst online: 14-11 22:30
Manuzhai schreef op maandag 13 november 2006 @ 16:51:
Ik zie veel Subversion-based projecten die een minder-stable trunk hebben waar alles in beginsel op ontwikkeld wordt, met daarnaast branches voor het stabiliseren van releases enzo. In mijn ervaring werkt dit echter het best als de trunk wel enigszins stable gehouden kan worden. Voor het echt disruptieve werk worden dan meestal wel branches gemaakt. Bedenk echter dat het parallel ontwikkelen van branches vaak wel redelijk wat onderhoud vergt (in termen van het mergen van wijzigingen van trunk naar de branch voor zolang als de branch apart wordt gehouden).
Daarentegen kost het enigzins stabiel houden van een trunk waar iedereen op zit te werken ook behoorlijk wat energie zo nu en dan. Eigenlijk zijn dat de nadelen van beiden aanpakken: als de trunk altijd stabiel moet zijn dan wordt er vrijwel per definitie uitsluitend functionaliteit toegevoegd in branches, waardoor je vrij vaak spullen moet mergen om ze in de trunk te krijgen. Als je met z'n allen op de trunk zit dan komt het echter regelmatig voor dat een developer spullen van iemand anders kapot maakt waardoor dat weer onderling moet worden geregeld.

Ik vermoed dat de altijd-stabiele-trunk meer energie kost omdat je bij die aanpak eigenlijk altijd overhead hebt en bij de niet-stabiele-trunk alleen overhead als mensen daadwerkelijk door elkaars spullen harken. Je hebt daarentegen met de eerste aanpak dankzij de stabiele trunk een overzichtelijkere omgeving. Maar hoe belangrijk dat is ligt aan allerlei omgevingsvariabelen.

  • H!GHGuY
  • Registratie: December 2002
  • Niet online

H!GHGuY

Try and take over the world...

als iedereen features implementeert en test in zijn eigen sandbox vooraleer te committen ben je ook al redelijk op weg om je trunk niet te hard te vervuilen

ASSUME makes an ASS out of U and ME


  • kenneth
  • Registratie: September 2001
  • Niet online

kenneth

achter de duinen

Met sandbox bedoel je de lokale working copy?

Look, runners deal in discomfort. After you get past a certain point, that’s all there really is. There is no finesse here.


  • JeroenB
  • Registratie: November 1999
  • Laatst online: 14-11 22:30
H!GHGuY schreef op dinsdag 14 november 2006 @ 18:37:
als iedereen features implementeert en test in zijn eigen sandbox vooraleer te committen ben je ook al redelijk op weg om je trunk niet te hard te vervuilen
Je moet dan alleen wel een soort mascotte gebruiken om aan te geven wie er aan het mergen is, anders loop je het risico dat je het een paar keer moet doen op een middag, of dat het dubbel wordt gedaan :) Als je dat onprettig vind, dan benoem je daar iemand voor en laat je iedereen vrolijk een branch maken. Kwestie van schaalbaarheid: vanaf een bepaalde grootte heb je voordeel bij zo'n functie, tot een bepaalde grootte kost het meer dan het oplevert. Wat die magische grens precies is, dat is een lastig probleem.

  • H!GHGuY
  • Registratie: December 2002
  • Niet online

H!GHGuY

Try and take over the world...

Als er teveel commits moeten gebeuren werken we met een chainmail.

iedereen zijn namen staan onder elkaar. De bovenste begint te committen en zendt de mail door naar de volgende. Die commit en zendt op zijn beurt de mail door naar de volgende. Tot iedereen klaar is.

Ook is het zo dat voor belangrijke commits/merges/builds een commit stop afgelast wordt.

ASSUME makes an ASS out of U and ME


  • EfBe
  • Registratie: Januari 2000
  • Niet online
Ik heb een stable versie (in de trunk), een dev versie (een branch) en alle oudere versies zijn tags. Wanneer er een nieuwe versie moet worden begonnen maak ik een branch van de trunk en develop in de branch. Wanneer deze klaar is wordt de trunk een tag, want dat is dan niet meer de stable versie, en maak ik de dev branch de trunk.

Ik merge zelden op deze manier en dat levert dus ook niet veel merge ellende op.

Creator of: LLBLGen Pro | Camera mods for games
Photography portfolio: https://fransbouma.com


  • MicroWhale
  • Registratie: Februari 2000
  • Laatst online: 01-12 10:46

MicroWhale

The problem is choice

Wij branchen evolutionair:

per product/onderdeel hebben we een folder/tree met daarin branches

werking:
- we beginnen met een branch (bijv. 1.00)
- we schedulen release date voor de publieke branches
- op de release date sluiten we de (werkende) branch en maken een final build
- deze wordt gereleased en we maken een nieuwe branch (bijv. 1.10) van de oude
- in de nieuwe branch werken we alle versienummers bij

builds:
- elke nacht draaien we een build van de laatste branch
- elke week eentje van de vorige branch
- als een nachtelijke build breekt wordt er getrakteerd; alles wat je incheckt moet dus werken

archief:
- na een x branches gooien we de oudste weg; zo houden we een history van ongeveer een jaar. alles wat daarvoor is gebeurd is obsolete.

Het enige belangrijke is dat je vandaag altijd rijker bent dan gisteren. Als dat niet in centen is, dan wel in ervaring.


  • EfBe
  • Registratie: Januari 2000
  • Niet online
Als je niet een nieuwe repository aanmaakt gooit SVN niets weg, dus je oude history van meer dan een jaar terug is gewoon nog aanwezig.

Creator of: LLBLGen Pro | Camera mods for games
Photography portfolio: https://fransbouma.com


  • MicroWhale
  • Registratie: Februari 2000
  • Laatst online: 01-12 10:46

MicroWhale

The problem is choice

EfBe schreef op donderdag 16 november 2006 @ 12:55:
Als je niet een nieuwe repository aanmaakt gooit SVN niets weg, dus je oude history van meer dan een jaar terug is gewoon nog aanwezig.
dat klopt, maar je kunt de toegang tot oudere branches blokkeren door er rechten op te zetten/af te halen.
Het is namelijk (bij ons) niet wenselijk om Gigabytes aan oude branches over te pompen bij het uitchecken.

Het enige belangrijke is dat je vandaag altijd rijker bent dan gisteren. Als dat niet in centen is, dan wel in ervaring.


  • kenneth
  • Registratie: September 2001
  • Niet online

kenneth

achter de duinen

Checken mensen dan de hele repo uit bij jullie :?

Look, runners deal in discomfort. After you get past a certain point, that’s all there really is. There is no finesse here.


  • Observer
  • Registratie: April 2001
  • Laatst online: 14:00
Wij werken met een "lenient variation" op "basically stable".

Niet kritieke bugfixes en kleine veranderingen aan de functionaliteit gebeuren op de trunk. Grote veranderingen ("projecten") krijgen een eigen branch.

Elke release-versie heeft ook een eigen branch van de trunk. Alle projecten die met een bepaalde release mee moet worden (na het testen van de projectbranches zelf) in de release branche gemerged waarna de regressie test plaatsvindt.
Enige tijd na de release (iig voordat aan het branchen van de nieuwe release begonnen wordt) wordt de oude release branch weer naar de trunk gemerged.

Bij langdurige projecten wordt regelmatig de trunk gemerged met de project branch, zodat het project niet teveel verschillen gaat krijgen met de laatste release versie.

Het mergen van projecten naar de release branch wordt gedaan door iemand die aan het project zelf heeft gewerkt, de release branch wordt teruggemerged naar de trunk door een ervaren developer op roulatie basis.

There are 10 kinds of people in the world: those that understand binary and those that don't


  • flowerp
  • Registratie: September 2003
  • Laatst online: 11-09 18:20
EfBe schreef op donderdag 16 november 2006 @ 08:37:
Ik heb een stable versie (in de trunk), een dev versie (een branch) en alle oudere versies zijn tags.
In principe is er in SVN geen verschil tussen een branch en een tag. Het zijn eigenlijk alleen historische namen.

Over onze aanpak; we werken uitsluitend serverside, maar houden puur kwa organisatie wel versie nummers aan. Tussendoor wordt er ook telkens van alles live gezet, en dat vertroebeld de strakke versie indeling een beetje. Hoe wij werken met SVN is weer een iets andere variant op "basically unstable":
  • Alle nieuwe development vind in de trunk plaats. Dit mag op plekken breken.
  • Twee weken voordat een versie live gaat maken we een branch met de naam van die versie, bv 1.37.
  • Gedurende die 2 weken wordt de code in de branch door QA onderzocht. Eventuele bugs worden in deze branch gefixt.
  • Aan het eind van de 2 weken gaat de branch live en worden in principe alle bugfixes terug gemerged naar de trunk.
  • Terwijl er wordt gewerkt aan de nieuwe versie (1.38) in de trunk, worden er continu bugfixes gedaan in de 1.37 branch die vrijwel meteen live gezet worden. Deze fixes worden ook teruggemerged naar de trunk.
  • Twee weken voordat de nieuwe versie live gaat, wordt er weer een nieuwe branch gemaakt, 1.38 in dit voorbeeld.
  • Gedurende deze twee weken zijn er dus 3 plekken waar gecommit kan worden: branch 1.37 die nu live staat (voor hele kritieke bugs), branch 1.38 die QA ondergaat, en de trunk waar alweer aan 1.39 wordt gewerkt.
  • Nadat 1.38 live is gegaan wordt 1.37 afgesloten en kan er niet meer gecommit worden.
Opzich werkt dit goed, maar er zijn nog wel wat problemen.

Vanwege een tekortkoming in SVN, kun je maar 1 keer 'automatisch' van een bepaalde branch naar de trunk mergen. Wil je een 2de keer mergen dan ziet SVN opeens allemaal conflicten die er eigenlijk niet zijn. Dit komt omdat SVN nergens bij houdt dat een file al eerder gemerged is. Dit is een bekend defect in SVN en op de officiele site is er ook zeer veel documentatie over te vinden. Op de middellange termijn zal "merge tracking" wel aan SVN toegevoegd worden, maar hoe lang dat nog duurt weet bijna niemand.

Op het moment dat wij de 'code freeze' doen en een branch maken, zouden er eigenlijk alleen bugfixes in die branch gedaan mogen worden. Het blijkt echter dat veel programmeurs de discipline niet op kunnen brengen om het bij bug fixes te houden. Onder het mom van, "dit is een klein dingetje dat nergens mee interfereerd", of "jaja, maar dit moet echt nog live en anders moeten we weer lang wachten", worden er toch nog snel even nieuwe features gecommit. Dit leverd dan ook niet zelden problemen op, omdat er code veranderd wordt die eigenlijk al door de QA heen gegaan is.

Hiernaast hebben we nog een strategie om de overlast van brekende code in de unstable trunk te beperken.

Een aantal onderdelen van de applicatie zijn ondergebracht in aparte sub projects, die ook als apart project in de repository staan. Het resultaat van elk zo'n project is een enkele jar file (we werken voornamelijk met Java), die wordt gecopieerd en gecommit in het hoofd project. Dit betekent dat er dus een tijdje in een sub project gecommit kan worden, en pas als het resultaat stabiel is wordt dit als binaire file gecommit in het hoofd project.

Als algemeen puntje voor SVN voor de Java developpers: er is eigenlijk nog geen goede SVN support voor Eclipse. Zowel subclipse als subversive hebben nog diverse problemen.

It's shocking to find how many people do not believe they can learn, and how many more believe learning to be difficult.


  • djc
  • Registratie: December 2001
  • Laatst online: 08-09 23:18

djc

flowerp schreef op zaterdag 18 november 2006 @ 00:32:
Vanwege een tekortkoming in SVN, kun je maar 1 keer 'automatisch' van een bepaalde branch naar de trunk mergen. Wil je een 2de keer mergen dan ziet SVN opeens allemaal conflicten die er eigenlijk niet zijn. Dit komt omdat SVN nergens bij houdt dat een file al eerder gemerged is. Dit is een bekend defect in SVN en op de officiele site is er ook zeer veel documentatie over te vinden. Op de middellange termijn zal "merge tracking" wel aan SVN toegevoegd worden, maar hoe lang dat nog duurt weet bijna niemand.
Je kunt het semi-officiële svnmerge.py gebruiken (te downloaden op de SVN-website) om merges te doen. Deze slaat op je branch op welke revisies al gemerged zijn en welke revisies je juist helemaal niet wil mergen (in properties), en doet zo al een vorm van merge-tracking.

Rustacean


  • flowerp
  • Registratie: September 2003
  • Laatst online: 11-09 18:20
Manuzhai schreef op zondag 19 november 2006 @ 10:51:
[...]
Je kunt het semi-officiële svnmerge.py gebruiken (te downloaden op de SVN-website) om merges te doen. Deze slaat op je branch op welke revisies al gemerged zijn en welke revisies je juist helemaal niet wil mergen (in properties), en doet zo al een vorm van merge-tracking.
Inderdaad, ik zat hier al naar te kijken. Als het goed is wordt dit script tegenwoordig ook meegeleverd met SVN 1.4. Ik wilde deze nog steeds eens een keer gaan uitproberen, maar ben er nog niet aan toegekomen.

Iemand hier die dit script echt gebruikt heeft?

It's shocking to find how many people do not believe they can learn, and how many more believe learning to be difficult.

Pagina: 1