Utrzymuj setki niestandardowych oddziałów w oddziale głównym


140

Obecnie mamy jedną gałąź główną dla naszej aplikacji PHP we wspólnym repozytorium. Mamy ponad 500 klientów, którzy są subskrybentami naszego oprogramowania, z których większość ma pewne dostosowania do różnych celów, każdy w oddzielnym oddziale. Dostosowaniem może być inna nazwa pola tekstowego, zupełnie nowa funkcja lub moduł albo nowe tabele / kolumny w bazie danych.

Wyzwanie, przed którym stoimy, polega na tym, że utrzymując setki niestandardowych oddziałów i dystrybuując je do klientów, od czasu do czasu udostępniamy nowe funkcje i aktualizujemy nasz oddział główny, a my chcemy przekazywać zmiany gałęzi master do oddziałów niestandardowych w celu aktualizacji je do najnowszej wersji.

Niestety często powoduje to wiele konfliktów w kodzie niestandardowym i spędzamy wiele godzin, przeglądając każdą gałąź, aby rozwiązać wszystkie konflikty. Jest to bardzo nieefektywne i stwierdziliśmy, że błędy nie są rzadkie przy rozwiązywaniu tych konfliktów.

Szukam bardziej wydajnego sposobu na aktualizowanie naszych gałęzi wydań klienta do gałęzi głównej, co przyniesie mniej wysiłku podczas łączenia.


11
Przykro nam, że nie udzielono odpowiedzi „możesz użyć narzędzia X”, ale nie ma takiej.
Wyścigi lekkości na orbicie

3
Lub podczas kompilacji (co jest prawdopodobnie bardziej powszechne). Po prostu ... nie całkiem osobno podstawy kodowania.
Wyścigi lekkości na orbicie

15
@ FernandoTan - Twoim widocznym objawem może być kod, ale główną przyczyną twojej choroby jest fragmentacja produktu, lekarstwo musi pochodzić z mapowania koncentracji / możliwości produktu, a nie czyszczenia kodu - to w końcu się stanie. W swojej odpowiedzi szczegółowo opisałem - programmers.stackexchange.com/a/302193/78582
Alex S

8
Może to również stanowić problem ekonomiczny. Czy naprawdę zarabiasz na tych wszystkich 500 klientach? Jeśli nie, musisz przemyśleć swój model wyceny i odrzucić wnioski o zmianę, jeśli klient nie uiści dodatkowej opłaty.
Christian Strempfer,

13
To sprawiło, że moje serce trochę się złamało. Na szczęście inni już wykrzykiwali prawidłowe odpowiedzi - moją jedyną dodatkową rekomendacją jest napisanie tego i przesłanie go do TheDailyWTF.
zxq9,

Odpowiedzi:


314

Całkowicie nadużywasz gałęzi! Dostosowanie powinno być oparte na elastyczności aplikacji, a nie elastyczności kontroli wersji (która, jak odkryłeś, nie jest przeznaczona / przeznaczona do tego rodzaju zastosowań).

Na przykład, aby etykiety pól tekstowych pochodziły z pliku tekstowego, a nie były zakodowane na stałe w aplikacji (w ten sposób działa internacjonalizacja). Jeśli niektórzy klienci mają różne funkcje, uczyń swoją aplikację modułową , z surowymi wewnętrznymi granicami regulowanymi przez rygorystyczne i stabilne interfejsy API, aby funkcje można było podłączyć w razie potrzeby.

Podstawowa infrastruktura i wszelkie wspólne funkcje muszą być przechowywane, konserwowane i testowane tylko raz .

Powinieneś to zrobić od samego początku. Jeśli masz już pięćset wariantów produktu (!), Naprawienie go będzie ogromną pracą… ale nie więcej niż bieżącą konserwacją.


142
+1 dla „Powinieneś to zrobić od samego początku”. Ten poziom długu technicznego może zniszczyć firmę.
Daenyth,

31
@Denenyth: Szczerze mówiąc z pięcioma niestandardowymi oddziałami jestem zdumiony, że jeszcze tego nie zrobił. Kto pozwala, aby sprawy potoczyły się tak źle? lol
Wyścigi lekkości na orbicie

73
@FernandoTan Tak mi przykro, więc bardzo mi przykro ...
Enderland

20
@FernandoTan: Ja też. :( Może powinieneś był zadawać więcej pytań podczas rozmowy kwalifikacyjnej?;) Dla jasności „ty” w mojej odpowiedzi to organizacja. To jest abstrakcja. Nie chcę obwiniać osób.
Wyścigi lekkości na orbicie

58
Najpierw uzyskaj więcej informacji: pozwól programistom odróżnić bieżącą wersję od dostosowanej gałęzi. Więc przynajmniej wiesz, jakie są różnice. Ta lista pozwala zobaczyć, gdzie możesz wygrać najszybszą redukcję oddziałów. Jeśli 50 ma niestandardowe nazwy pól, skoncentruj się na nich, a zaoszczędzisz 50 oddziałów. Następnie poszukaj następnego. Możesz też mieć takie, których nie można przywrócić, ale wtedy przynajmniej kwota będzie niższa i nie będzie rosła, gdy zdobędziesz więcej klientów.
Luc Franken,

93

Posiadanie 500 klientów to miły problem, jeśli spędziłeś czas z góry, aby uniknąć tego problemu z oddziałami, być może nigdy nie byłbyś w stanie handlować wystarczająco długo, aby zdobyć żadnych klientów.

Po pierwsze, mam nadzieję, że obciążysz swoich klientów wystarczająco, aby pokryć WSZYSTKIE koszty utrzymania ich niestandardowych wersji. Zakładam, że klienci oczekują, że otrzymają nowe wersje bez konieczności płacenia za ponowne dostosowanie. Zacznę od znalezienia wszystkich plików, które są takie same w 95% twoich oddziałów. To 95% to stabilna część twojej aplikacji.

Następnie znajdź wszystkie pliki, które mają tylko kilka linii różniących się między gałęziami - spróbuj wprowadzić system konfiguracji, aby różnice te można było usunąć. Na przykład zamiast 100 plików z różnymi polami tekstowymi, masz 1 plik konfiguracyjny, który może zastąpić dowolną etykietę tekstową. (Nie trzeba tego robić za jednym razem, wystarczy skonfigurować etykietę pola tekstowego za pierwszym razem, gdy klient chce ją zmienić).

Następnie przejdź do trudniejszych problemów przy użyciu wzorca strategii, wstrzyknięcia zależności itp.

Rozważ przechowywanie jsonów w bazie danych zamiast dodawania kolumn do pól własnych klienta - może to działać, jeśli nie musisz przeszukiwać tych pól za pomocą SQL.

Za każdym razem, gdy sprawdzasz plik w gałęzi, MUSISZ różnicować go za pomocą main i uzasadniać każdą zmianę, w tym spację. Wiele zmian nie będzie potrzebnych i można je usunąć przed zameldowaniem. Może to wynikać tylko z tego, że jeden programista ma w swoim edytorze różne ustawienia dotyczące formatowania kodu.

Zamierzasz najpierw przejść od 500 oddziałów z dużą ilością plików, które są różne, do większości oddziałów mających tylko kilka plików, które są różne. Zarabiając wciąż wystarczająco dużo pieniędzy, aby żyć.

Przez wiele lat możesz mieć 500 oddziałów, ale jeśli są one łatwiejsze w zarządzaniu, to wygrałeś.


Na podstawie komentarza br3w5:

  • Możesz wziąć każdą klasę, która różni się między klientami
  • Utwórz „xxx_baseclass”, który definiuje wszystkie metody wywoływane w klasie spoza niej
  • Zmień nazwę klasy, aby xxx nazywało się xxx_clientName (jako podklasę xxx_baseclass)
  • Użyj wstrzykiwania zależności, aby dla każdego klienta była używana poprawna wersja klasy
  • A teraz dla sprytnego wglądu wymyślił br3w5! Użyj narzędzia do analizy kodu statycznego, aby znaleźć zduplikowany kod i przenieś go do klasy podstawowej itp

Wykonaj powyższe czynności dopiero po zdobyciu łatwego ziarna i prześledź je najpierw kilkoma klasami.


28
+1 za próbę rozwiązania problemu
Ian

35
Bardzo się martwiłem, że gratulujesz sobie odpowiedzi, dopóki nie zdałem sobie sprawy, że nie jesteś tym samym @Ian, który napisał odpowiedź.
Theron Luhn,

2
Może powinni użyć statycznego narzędzia do analizy kodu, aby zawęzić zakres części kodu, które są duplikowane (po zidentyfikowaniu wszystkich plików, które są takie same)
br3w5

1
Również tworzenie wersjonowanych pakietów, aby pomóc zespołowi śledzić, który klient ma daną wersję kodu
br3w5

1
To brzmi jak długi, zawstydzony sposób powiedzenia „po prostu refaktoryzuj swój kod”
Roland Tepp

40

W przyszłości zadaj pytania testowe Joela w swoim wywiadzie. Bardziej prawdopodobne jest, że nie wejdziesz do wraku pociągu.


To jest, ach, jak to powiedzieć ... naprawdę, naprawdę zły problem. „Stopa procentowa” tego długu technicznego będzie bardzo, bardzo wysoka. To może nie być możliwe do odzyskania ...

Jak zintegrowane z „rdzeniem” są te niestandardowe zmiany? Czy możesz zrobić z nich własną bibliotekę i mieć jeden „rdzeń”, a każdy konkretny klient ma swój „dodatek”?

Czy te wszystkie bardzo małe konfiguracje?

Myślę, że rozwiązaniem jest połączenie:

  • Zmiana wszystkich zakodowanych na stałe zmian w elementy oparte na konfiguracji. W tym przypadku każdy ma tę samą podstawową aplikację, ale użytkownicy (lub ty) włączają / wyłączają funkcje, ustawiają nazwy itp. W razie potrzeby
  • Przenoszenie funkcjonalności / modułów „specyficznych dla klienta” do oddzielnych projektów, więc zamiast jednego „projektu” masz jeden „główny projekt” z modułami, które możesz łatwo dodawać / usuwać. Alternatywnie możesz również wprowadzić te opcje konfiguracji.

Żadne nie będzie banalne, jakbyś znalazł się tutaj z ponad 500 klientami, prawdopodobnie nie zrobiłeś w tym żadnego prawdziwego rozróżnienia. Oczekuję, że twoje zmiany w rozdzieleniu tego będą bardzo czasochłonne.

Podejrzewam również, że będziesz miał poważne problemy z łatwym wyodrębnieniem i kategoryzowaniem całego kodu specyficznego dla klienta.

Jeśli większość zmian są szczegółowo różnice brzmieniem Proponuję pytania czytania jak to o lokalizacji językowej. Niezależnie od tego, czy robisz wiele języków w całości, czy tylko w podzbiorze, rozwiązanie jest takie samo. Dotyczy to w szczególności PHP i lokalizacji.


1
Ponadto, ponieważ będzie to ogromne zadanie (delikatnie mówiąc), poważnym wyzwaniem będzie nawet przekonanie kierowników do poświęcenia dużej ilości czasu i pieniędzy na ten problem. @FernandoTan Na tej stronie mogą znajdować się pytania + odpowiedzi, które mogą pomóc w rozwiązaniu tego konkretnego problemu.
Radu Murzea,

10
Które pytanie z testu joel powiedziałoby ci, że firma nadużywa oddziałów?
SpaceTrucker,

2
@SpaceTrucker: „Czy robisz codzienne kompilacje?” mógł pomóc. Mając 500 oddziałów, prawdopodobnie ich nie posiadali lub mogliby wspomnieć, że robią to tylko dla niektórych oddziałów.
sleske,

17

Jest to jeden z najgorszych anty-wzorów, które można trafić dowolnym VCS.

Prawidłowym podejściem jest tutaj przekształcenie niestandardowego kodu w coś napędzanego przez konfigurację, a następnie każdy klient może mieć własną konfigurację, zapisaną na stałe w pliku konfiguracyjnym lub w bazie danych lub w innej lokalizacji. Możesz włączyć lub wyłączyć całe funkcje, dostosować wygląd odpowiedzi i tak dalej.

Pozwala to zachować jedną gałąź główną z kodem produkcyjnym.


3
Jeśli to zrobisz, zrób sobie przysługę i postaraj się jak najlepiej wykorzystać wzór strategii . Ułatwi to utrzymanie twojego kodu, niż gdybyś po prostu działał przez if(getFeature(FEATURE_X).isEnabled())cały czas.
TMN

13

Celem oddziałów jest zbadanie jednej możliwej ścieżki rozwoju bez ryzyka naruszenia stabilności głównej gałęzi. Powinny one ostatecznie zostać połączone w odpowiednim czasie lub odrzucone, jeśli prowadzą do ślepej uliczki. To, co masz, to nie tyle gałęzi, co raczej 500 widelców tego samego projektu i próba zastosowania wszystkich istotnych zestawów zmian do wszystkich z nich to syzyfowe zadanie.

Zamiast tego powinieneś zamiast tego mieć swój podstawowy kod w swoim własnym repozytorium, z niezbędnymi punktami wejścia do modyfikowania zachowania poprzez konfigurację i wstrzykiwania zachowania, na co pozwalają odwrócone zależności .

Różne konfiguracje, które masz dla klientów, mogą albo po prostu odróżnić się po jakimś zewnętrznie skonfigurowanym stanie (np. Baza danych) lub, jeśli to konieczne, żyć jako osobne repozytoria, które dodają rdzeń jako podmoduł.


6
Zapomniałeś o gałęziach obsługi technicznej, które są w zasadzie przeciwieństwem gałęzi opisanych w odpowiedzi. :)
Wyścigi lekkości na orbicie

7

Wszystkie ważne rzeczy zostały tutaj zaproponowane przez dobre odpowiedzi. Chciałbym dodać moje pięć pensów jako sugestię procesu.

Chciałbym zasugerować rozwiązanie tego problemu w długim lub średnim okresie i przyjęcie zasad, w jaki sposób opracowywać kod. Spróbuj zostać elastycznym zespołem do nauki. Jeśli ktoś może mieć 500 repozytoriów zamiast konfigurować oprogramowanie, nadszedł czas, aby zadać sobie pytanie, jak dotychczas pracowałeś i zrobisz to od teraz.

Co znaczy:

  1. Wyjaśnij obowiązki zarządzania zmianami: jeśli klient potrzebuje pewnych dostosowań, kto je sprzedaje, kto im pozwala i kto decyduje o sposobie zmiany kodu? Gdzie należy wkręcić śruby, jeśli niektóre rzeczy wymagają zmiany?
  2. Wyjaśnij rolę, kto w twoim zespole może dokonywać nowych repozytoriów, a kto nie.
  3. Postaraj się upewnić, że wszyscy w twoim zespole dostrzegają konieczność wzorców, które pozwalają na elastyczność oprogramowania.
  4. Wyjaśnij swoje narzędzie zarządzania: skąd szybko wiesz, jaki klient ma jakie adopcje kodu. Wiem, że niektóre „listy 500” brzmią denerwująco, ale tutaj jest trochę „ekonomii emocjonalnej”, jeśli chcesz. Jeśli nie możesz szybko powiedzieć o zmianach klienta, czujesz się jeszcze bardziej zagubiony i narysowany, jakbyś musiał założyć listę. Następnie użyj tej listy, aby pogrupować funkcje w sposób pokazany tutaj przez inne osoby:
    • grupuj klientów według drobnych / poważnych zmian
    • grupuj według zmian związanych z tematem
    • grupuj według zmian łatwych do scalenia i zmian trudnych do scalenia
    • znajdź grupy równych zmian dokonanych w kilku repozytoriach (o tak, będą pewne).
    • być może najważniejsze w rozmowie ze swoim menedżerem / inwestorem: grupuj według drogich zmian i tanich zmian.

W żaden sposób nie ma to na celu stworzenia atmosfery złego ciśnienia w Twoim zespole. Raczej sugeruję, abyś najpierw wyjaśnił sobie te kwestie i, gdziekolwiek poczujesz wsparcie, zorganizuj to razem ze swoim zespołem. Zaproś osoby przyjazne do stołu, aby poprawić swoje wrażenia.

Następnie spróbuj ustanowić długoterminowe okno czasowe, w którym gotujesz to na małym płomieniu. Sugestia: spróbuj scalić co najmniej dwa repozytoria co tydzień, a zatem usuń co najmniej jedno . Możesz się tego często nauczyć, możesz połączyć więcej niż dwie gałęzie, uzyskując rutynę i nadzór. W ten sposób w ciągu jednego roku możesz zająć się najgorszymi (najdroższymi?) Oddziałami, a za dwa lata możesz zmniejszyć ten problem, aby mieć wyraźnie lepsze oprogramowanie. Ale nie oczekuj więcej, ponieważ ostatecznie nikt nie będzie miał na to czasu, ale to Ty nie będziesz już na to pozwalać, ponieważ jesteś architektem oprogramowania.

W ten sposób spróbowałbym sobie z tym poradzić, gdybym był na twojej pozycji. Nie wiem jednak, w jaki sposób Twój zespół zaakceptuje takie rzeczy, w jaki sposób oprogramowanie na to naprawdę pozwala, w jaki sposób otrzymujesz wsparcie, a także czego jeszcze musisz się nauczyć. Jesteś architektem oprogramowania - po prostu idź :-)


2
Dobre punkty na temat rozwiązywania problemów społecznych / organizacyjnych kryjących się za problemami technicznymi. Jest to zbyt często pomijane.
sleske

5

Kontrastując wszystkich nieprzyzwoitych mówców, załóżmy prawdziwą potrzebę biznesową.

(na przykład kodem dostarczanym jest kod źródłowy, klienci pochodzą z tej samej branży, a zatem są ze sobą konkurenci, a model biznesowy obiecuje zachować tajemnicę)

Ponadto załóżmy, że Twoja firma posiada narzędzia do utrzymania wszystkich oddziałów, czyli albo siły roboczej (powiedzmy 100 programistów zaangażowanych w łączenie, zakładając 5-dniowe opóźnienie wydania; lub 10 programistów zakładających, że 50-dniowe opóźnienie wydania jest OK), lub tak niesamowite zautomatyzowane testy, że automatyczne połączenia są naprawdę testowane zarówno pod kątem specyfikacji podstawowej, jak i specyfikacji rozszerzenia w każdej branży, a zatem tylko zmiany, które nie łączą się „czysto”, wymagają interwencji człowieka. Jeśli klienci płacą nie tylko za dostosowania, ale za ich utrzymanie, może to być prawidłowy model biznesowy.

Moje (i nie-mówcy) pytanie brzmi: czy masz dedykowaną osobę odpowiedzialną za dostawę do każdego klienta? Jeśli jesteś, powiedzmy, firmą liczącą 10 000 osób, może tak być.

W niektórych przypadkach może to być obsługiwane przez architekturę wtyczek , powiedzmy, że twoim rdzeniem jest pień, wtyczki mogą być przechowywane w pniu lub gałęziach, a konfiguracja dla każdego klienta jest plikiem o unikalnej nazwie lub jest przechowywana w oddziale klienta.

Wtyczki mogą być ładowane w czasie wykonywania lub wbudowane w czasie kompilacji.

Naprawdę wiele projektów jest wykonywanych w ten sposób, nadal występuje zasadniczo ten sam problem - proste podstawowe zmiany są trywialne w integracji, zmiany konfliktu muszą zostać wycofane lub zmiany w wielu wtyczkach.

Zdarzają się przypadki, gdy wtyczki nie są wystarczająco dobre, wtedy tak wiele wewnętrznych elementów rdzenia musi zostać poprawionych, że liczba interfejsów wtyczek staje się zbyt duża, aby poradzić sobie.

Idealnie byłoby to obsługiwane przez programowanie aspektowe , w którym trunk jest kodem podstawowym, a gałęzie są aspektami (to jest dodatkowy kod i instrukcje, jak podłączyć dodatki do rdzenia)

Prosty przykład, możesz określić, że niestandardowy foojest uruchamiany przed rdzeniem lub po klass.foonim, że zastępuje go, lub że otacza go i może zmieniać dane wejściowe lub wyjściowe.

Jest na to mnóstwo bibliotek, jednak problem łączenia konfliktów nie ustępuje - czyste połączenia są obsługiwane przez AOP, a konflikty nadal wymagają interwencji człowieka.

Wreszcie, taki biznes naprawdę musi zajmować się utrzymaniem oddziału , a mianowicie, czy funkcja X specyficzna dla klienta jest tak powszechna, że ​​przeniesienie jej do rdzenia jest tańsze, chociaż nie wszyscy klienci płacą za to?


3

Nie rozwiązujesz przyczyny choroby, patrząc na objaw. Stosowanie podejścia „zarządzania kodem” jest objawowe, ale nie rozwiąże problemu na dłuższą metę. Główną przyczyną jest brak „dobrze zarządzanych” możliwości produktu, funkcji oraz ich rozszerzeń i odmian.

Twój „niestandardowy” kod reprezentuje jedynie rozszerzenia funkcji i możliwości produktu oraz zmiany pól danych w innych.

Jak szerokie są funkcje niestandardowe, jak różne, jak kontekstowo podobne, czy nie, będą miały duży wpływ na „odkażanie” bazy kodu produktu.

To nie tylko sposób kodowania i wersji - to miejsce, w którym odgrywa rolę zarządzanie produktem, architektura produktu i architektura danych . Poważnie.

Ponieważ pod koniec dnia kod jest niczym innym, jak oferowaniem klientom funkcji biznesowych i produktów / usług . Za to firma otrzymuje wynagrodzenie.

Lepsze zrozumienie tego musi wynikać z punktu widzenia „możliwości”, a nie z punktu widzenia kodu.

Ty, Twoja firma i produkt nie może być wszystkim dla wszystkich. Teraz, gdy masz przyzwoitą bazę przychodów wynoszącą 500 klientów, nadszedł czas, aby wyprodukować to, co zamierzasz być.

A jeśli oferujesz kilka rzeczy, sensowne byłoby zmodularyzowanie możliwości produktu w zorganizowany sposób.

Jak szerokie i głębokie będą twoje produkty? W przeciwnym razie doprowadzi to do problemów związanych z „jakością usług” oraz „rozwodnieniem i rozdrobnieniem produktu”.

Będziesz CRM lub ERP lub kolejność przetwarzania / wysyłki lub Microsoft Excel?

Istniejące rozszerzenia trzeba zakasać i harmonizować, tak duża oprogramowania główne ściąga i łączy produkty nabyte od uruchomienia.

Musisz mieć silną osobę zarządzającą produktem i architekturę danych mapującą następujące elementy:

  • Oddział główny, jego możliwości produktu i baza funkcji
  • Niestandardowe funkcje, typy i odmiany rozszerzeń
  • Znaczenie i różnorodność „pól niestandardowych”

... aby stworzyć mapę drogową asymilacji i harmonizacji wszystkich tych luźnych wątków / gałęzi produktów w wielkim kontekście swojej podstawowej aplikacji.

PS: Połącz się ze mną, znam osobę, która może pomóc Ci to naprawić :)


-5

Mogę się z tym odnosić. Podjąłem wiele projektów. W rzeczywistości 90% naszych prac rozwojowych polega na naprawianiu takich rzeczy. Nie każdy jest doskonały, więc sugeruję, abyś używał kontroli wersji we właściwy sposób i gdzie jesteś, jeśli to możliwe, możesz wykonać następujące czynności.

  • Odtąd, kiedy klient poprosi o aktualizację, przenieś go do nowego repozytorium.
  • Jeśli chcesz je scalić, to zrób to jako pierwszy i rozwiąż konflikty.
  • Następnie zarządzaj ich problemami i sprintami za pomocą ich repozytorium i utrzymuj te w trybie głównym, które chcesz uruchomić w trybie głównym. Może to obciążyć kolejne cykle wydawania, ale z czasem cię to uratuje.
  • Utrzymuj główną gałąź głównego repozytorium dla nowych klientów, a główne repozytorium powinno mieć tylko te gałęzie, nad którymi pracujesz na przyszłość. Starsze oddziały można następnie usunąć po migracji do repozytoriów klientów.

Osobiście zaimportowałem repozytorium z GitHub z 40 oddziałami do Bitbucket i utworzyłem 40 repozytoriów. Zajęło to tylko cztery godziny. To były odmiany motywów WordPress, więc push i pull były szybkie.

Istnieje wiele powodów, dla których „nie robi się dobrze za pierwszym razem” i myślę, że ci, którzy je szybko zaakceptują i przejdą do „zrób to dobrze tym razem”, zawsze będą odnosić sukcesy.


16
W jaki sposób wiele repozytoriów ułatwiłoby konserwację?
Mathletics,

W niektórych przypadkach, takich jak nasza, klienci muszą mieć dostęp do każdego repozytorium i zarządzać własnymi problemami, gdy staje się ono dostosowanym rozwiązaniem, aby mieli swoje własne repozytorium, co ułatwia zarządzanie i, jak powiedziałem, są to odmiany motywów WordPress, które działały dobrze. W wielu przypadkach może nie działać.
Farrukh Subhani
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.