OC.L10N.register( "lib", { "Cannot write into \"config\" directory!" : "Kan inte skriva till \"config\" katalogen!", "This can usually be fixed by giving the webserver write access to the config directory" : "Detta kan vanligtvis åtgärdas genom att ge skrivrättigheter till config-katalogen", "See %s" : "Se %s", "Or, if you prefer to keep config.php file read only, set the option \"config_is_read_only\" to true in it." : "Eller, om du föredrar att behålla config.php skrivskyddad, sätt alternativet \"config_is_read_only\" till true i den.", "This can usually be fixed by giving the webserver write access to the config directory. See %s" : "Detta fixas vanligtvis genom att ge webbservern skrivrättigheter till konfigureringsmappen. Se %s", "Or, if you prefer to keep config.php file read only, set the option \"config_is_read_only\" to true in it. See %s" : "Eller, om du föredrar att behålla config.php skrivskyddad, sätt alternativet \"config_is_read_only\" till true i den. Se %s", "The files of the app %1$s were not replaced correctly. Make sure it is a version compatible with the server." : "Filerna i appen %1$s ersattes inte korrekt. Kontrollera att det är en version som är kompatibel med servern.", "Sample configuration detected" : "Exempel-konfiguration detekterad", "It has been detected that the sample configuration has been copied. This can break your installation and is unsupported. Please read the documentation before performing changes on config.php" : "Det har detekterats att exempel-konfigurationen har kopierats. Detta kan förstöra din installation och stöds ej. Vänligen läs dokumentationen innan ändringar på config.php utförs", "%1$s and %2$s" : "%1$s och %2$s", "%1$s, %2$s and %3$s" : "%1$s, %2$s och %3$s", "%1$s, %2$s, %3$s and %4$s" : "%1$s, %2$s, %3$s och %4$s", "%1$s, %2$s, %3$s, %4$s and %5$s" : "%1$s, %2$s, %3$s, %4$s och %5$s", "Education Edition" : "Utbildningspaketet", "Enterprise bundle" : "Företagspaketet", "Groupware bundle" : "Groupwarepaket", "Social sharing bundle" : "Kommunikationspaketet ", "PHP %s or higher is required." : "PHP %s eller högre krävs.", "PHP with a version lower than %s is required." : "PHP med version lägre än %s krävs.", "%sbit or higher PHP required." : "%sbit eller nyare PHP-version krävs.", "Following databases are supported: %s" : "Följande databastyper stöds: %s", "The command line tool %s could not be found" : "Kommandoradsverktyget %s hittades inte.", "The library %s is not available." : "Biblioteket %s är inte tillgängligt.", "Library %1$s with a version higher than %2$s is required - available version %3$s." : "Bibliotek %1$s med en högre version än %2$s krävs - tillgänglig version %3$s.", "Library %1$s with a version lower than %2$s is required - available version %3$s." : "Bibliotek %1$s med en lägre version än %2$s krävs - tillgänglig version %3$s.", "Following platforms are supported: %s" : "Följande plattformar stöds: %s", "Server version %s or higher is required." : "Serverversion %s eller nyare krävs.", "Server version %s or lower is required." : "Serverversion %s eller äldre krävs.", "Logged in user must be an admin or sub admin" : "Inloggad användare måste vara administratör eller del-administratör", "Logged in user must be an admin" : "Inloggad användare måste vara administratör", "Wiping of device %s has started" : "Rensning av enhet %s har startat", "Wiping of device »%s« has started" : "Rensning av enhet »%s« har startat", "»%s« started remote wipe" : "»%s« startade fjärrensning", "Device or application »%s« has started the remote wipe process. You will receive another email once the process has finished" : "Enhet eller applikation »%s« har startat fjärrensning. Du kommer att få ett nytt mail när processen är klar", "Wiping of device %s has finished" : "Rensning av enhet %s har slutförts", "Wiping of device »%s« has finished" : "Rensning av enhet »%s« har slutförts", "»%s« finished remote wipe" : "»%s« slutförde fjärrensning", "Device or application »%s« has finished the remote wipe process." : "Enhet eller applikation »%s« har slutfört fjärrensning.", "Remote wipe started" : "Fjärrensning startad", "A remote wipe was started on device %s" : "Fjärrensning startades på enhet %s", "Remote wipe finished" : "Fjärrensning klar", "The remote wipe on %s has finished" : "Fjärrensning av enhet %s har slutörts", "Authentication" : "Autentisering", "Unknown filetype" : "Okänd filtyp", "Invalid image" : "Ogiltig bild", "Avatar image is not square" : "Profilbilden är inte fyrkantig", "today" : "idag", "tomorrow" : "imorgon", "yesterday" : "igår", "_in %n day_::_in %n days_" : ["om %n dag","om %n dagar"], "_%n day ago_::_%n days ago_" : ["%n dag sedan","%n dagar sedan"], "next month" : "nästa månad", "last month" : "förra månaden", "_in %n month_::_in %n months_" : ["om %n månad","om %n månader"], "_%n month ago_::_%n months ago_" : ["%n månad sedan","%n månader sedan"], "next year" : "nästa år", "last year" : "förra året", "_in %n year_::_in %n years_" : ["om %n år","om %n år"], "_%n year ago_::_%n years ago_" : ["%n år sedan","%n år sedan"], "_in %n hour_::_in %n hours_" : ["om %n timme","om %n timmar"], "_%n hour ago_::_%n hours ago_" : ["%n timme sedan","%n timmar sedan"], "_in %n minute_::_in %n minutes_" : ["om %n minut","om %n minuter"], "_%n minute ago_::_%n minutes ago_" : ["%n minut sedan","%n minuter sedan"], "in a few seconds" : "om några sekunder", "seconds ago" : "sekunder sedan", "Module with ID: %s does not exist. Please enable it in your apps settings or contact your administrator." : "Modul med ID: %s finns inte längre. Vänligen aktivera det i dina appinställningar eller kontakta din administratör.", "File name is a reserved word" : "Filnamnet är ett reserverat ord", "File name contains at least one invalid character" : "Filnamnet innehåller minst ett ogiltigt tecken", "File name is too long" : "Filnamnet är för långt", "Dot files are not allowed" : "Dot-filer är inte tillåtna", "Empty filename is not allowed" : "Tomma filnamn är inte tillåtna", "App \"%s\" cannot be installed because appinfo file cannot be read." : "Applikationen \"%s\" kan ej installeras eftersom informationen från appen ej kunde läsas.", "App \"%s\" cannot be installed because it is not compatible with this version of the server." : "Applikationen \"%s\" kan ej installeras eftersom den inte är kompatibel med denna serverversion.", "__language_name__" : "Svenska", "This is an automatically sent email, please do not reply." : "Detta är ett automatiskt skickat e-postmeddelande, svara inte på detta meddelande.", "Help" : "Hjälp", "Apps" : "Appar", "Settings" : "Inställningar", "Log out" : "Logga ut", "Users" : "Användare", "Unknown user" : "Okänd användare", "Overview" : "Översikt", "Basic settings" : "Generella inställningar", "Sharing" : "Delning", "Security" : "Säkerhet", "Groupware" : "Grupprogram", "Additional settings" : "Övriga inställningar", "Personal info" : "Personlig information", "Mobile & desktop" : "Mobil & skrivbord", "%s enter the database username and name." : "%s ange användarnamn och namn för databasen.", "%s enter the database username." : "%s ange databasanvändare.", "%s enter the database name." : "%s ange databasnamn", "%s you may not use dots in the database name" : "%s du får inte använda punkter i databasnamnet", "Oracle connection could not be established" : "Oracle-anslutning kunde inte etableras", "Oracle username and/or password not valid" : "Oracle-användarnamnet och/eller lösenordet är felaktigt", "PostgreSQL username and/or password not valid" : "PostgreSQL-användarnamnet och/eller lösenordet är felaktigt", "You need to enter details of an existing account." : "Du måste ange inloggningsuppgifter av ett aktuellt konto.", "Mac OS X is not supported and %s will not work properly on this platform. Use it at your own risk! " : "Mac OS X stöds inte och %s kommer inte att fungera korrekt på denna plattform. Använd på egen risk!", "For the best results, please consider using a GNU/Linux server instead." : "För bästa resultat, överväg att använda en GNU/Linux server istället.", "It seems that this %s instance is running on a 32-bit PHP environment and the open_basedir has been configured in php.ini. This will lead to problems with files over 4 GB and is highly discouraged." : "Det verkar som om denna %s instans körs på en 32-bitars PHP miljö och open_basedir har konfigurerats i php.ini. Detta kommer att leda till problem med filer över 4 GB och är verkligen inte rekommenderat!", "Please remove the open_basedir setting within your php.ini or switch to 64-bit PHP." : "Ta bort open_basedir i din php.ini eller byt till 64-bitars PHP.", "Set an admin username." : "Ange ett användarnamn för administratören.", "Set an admin password." : "Ange ett administratörslösenord.", "Can't create or write into the data directory %s" : "Kan inte skapa eller skriva till data-katalogen %s", "Invalid Federated Cloud ID" : "Ogiltigt federerat moln-ID", "Sharing backend %s must implement the interface OCP\\Share_Backend" : "Delningsgränssnittet %s måste implementera gränssnittet OCP\\Share_Backend", "Sharing backend %s not found" : "Delningsgränssnittet %s hittades inte", "Sharing backend for %s not found" : "Delningsgränssnittet för %s hittades inte", "%1$s shared »%2$s« with you and wants to add:" : "%1$s delade »%2$s« med dig och vill lägga till:", "%1$s shared »%2$s« with you and wants to add" : "%1$s delade »%2$s« med dig och vill lägga till", "»%s« added a note to a file shared with you" : "»%s« la till en kommentar till en fil delad med dig", "Open »%s«" : "Öppna »%s«", "%1$s via %2$s" : "%1$s via %2$s", "You are not allowed to share %s" : "Du har inte rätt att dela %s", "Can’t increase permissions of %s" : "Kan inte öka rättigheterna för %s", "Files can’t be shared with delete permissions" : "Filer kan inte delas med borttagningsrättigheter", "Files can’t be shared with create permissions" : "Filer kan inte delas med rättigheter att skapa", "Expiration date is in the past" : "Utgångsdatum är i det förflutna", "Can’t set expiration date more than %s days in the future" : "Kan inte sätta utgångsdatum mer än %s dagar framåt", "%1$s shared »%2$s« with you" : "%1$s delade »%2$s« med dig", "%1$s shared »%2$s« with you." : "%1$s delade »%2$s« med dig.", "Click the button below to open it." : "Klicka på knappen nedan för att öppna det.", "The requested share does not exist anymore" : "Den begärda delningen finns inte mer", "Could not find category \"%s\"" : "Kunde inte hitta kategorin \"%s\"", "Sunday" : "Söndag", "Monday" : "Måndag", "Tuesday" : "Tisdag", "Wednesday" : "Onsdag", "Thursday" : "Torsdag", "Friday" : "Fredag", "Saturday" : "Lördag", "Sun." : "Sön.", "Mon." : "Mån.", "Tue." : "Tis.", "Wed." : "Ons.", "Thu." : "Tors.", "Fri." : "Fre.", "Sat." : "Lör.", "Su" : "Sö", "Mo" : "Må", "Tu" : "Ti", "We" : "On", "Th" : "To", "Fr" : "Fr", "Sa" : "Lö", "January" : "Januari", "February" : "Februari", "March" : "Mars", "April" : "April", "May" : "Maj", "June" : "Juni", "July" : "Juli", "August" : "Augusti", "September" : "September", "October" : "Oktober", "November" : "November", "December" : "December", "Jan." : "Jan.", "Feb." : "Feb.", "Mar." : "Mar.", "Apr." : "Apr.", "May." : "Maj.", "Jun." : "Jun.", "Jul." : "Jul.", "Aug." : "Aug.", "Sep." : "Sep.", "Oct." : "Okt.", "Nov." : "Nov.", "Dec." : "Dec.", "Only the following characters are allowed in a username: \"a-z\", \"A-Z\", \"0-9\", and \"_.@-'\"" : "Endast följande tecken är tillåtna i användarnamnet: \"a-z\", \"A-Z\", \"0-9\", and \"_.@-'\"", "A valid username must be provided" : "Ett giltigt användarnamn måste anges", "Username contains whitespace at the beginning or at the end" : "Användarnamnet består av ett mellanslag i början eller i slutet", "Username must not consist of dots only" : "Användarnamnet får inte innehålla enbart punkter", "A valid password must be provided" : "Ett giltigt lösenord måste anges", "The username is already being used" : "Användarnamnet används redan", "Could not create user" : "Kunde inte skapa användare", "User disabled" : "Användare inaktiverad", "Login canceled by app" : "Inloggningen avbruten av appen", "App \"%1$s\" cannot be installed because the following dependencies are not fulfilled: %2$s" : "Appen \"%1$s\" kan inte installeras eftersom följande beroenden inte är uppfyllda: %2$s", "a safe home for all your data" : "En säker lagringsplats för all din data", "File is currently busy, please try again later" : "Filen är för tillfället upptagen, vänligen försök igen senare", "Can't read file" : "Kan ej läsa filen", "Application is not enabled" : "Applikationen är inte aktiverad", "Authentication error" : "Fel vid autentisering", "Token expired. Please reload page." : "Ogiltig token. Ladda om sidan.", "No database drivers (sqlite, mysql, or postgresql) installed." : "Inga databasdrivrutiner (sqlite, mysql, eller postgresql) installerade.", "Cannot write into \"config\" directory" : "Kan inte skriva till \"config\" katalogen", "Cannot write into \"apps\" directory" : "Kan inte skriva till \"apps\" katalogen!", "This can usually be fixed by giving the webserver write access to the apps directory or disabling the appstore in the config file. See %s" : "Detta kan vanligtvis fixas genom att ge webbservern skrivåtkomst till mappen för appar eller genom att avaktivera App store i konfigurationsfilen. Se %s", "Cannot create \"data\" directory" : "Kan inte skapa \"datamapp\"", "This can usually be fixed by giving the webserver write access to the root directory. See %s" : "Detta kan vanligtvis fixas genom att ge webbservern skrivåtkomst till rotkatalogen. Se %s", "Permissions can usually be fixed by giving the webserver write access to the root directory. See %s." : "Rättigheter kan vanligtvis fixas genom att ge webbservern skrivåtkomst till rotkatalogen. Se %s.", "Setting locale to %s failed" : "Sätta locale till %s misslyckades", "Please install one of these locales on your system and restart your webserver." : "Vänligen installera en av dessa locale på din server och starta om din webbserver,", "Please ask your server administrator to install the module." : "Vänligen be din administratör att installera modulen.", "PHP module %s not installed." : "PHP-modulen %s är inte installerad.", "PHP setting \"%s\" is not set to \"%s\"." : "PHP-inställning \"%s\" är inte inställd på \"%s\".", "Adjusting this setting in php.ini will make Nextcloud run again" : "Att ändra denna inställning i php.ini kommer göra så att Nextcloud fungerar igen", "mbstring.func_overload is set to \"%s\" instead of the expected value \"0\"" : "mbstring.func_overload är satt till \"%s\" istället för det förväntade värdet \"0\"", "To fix this issue set mbstring.func_overload to 0 in your php.ini" : "För att åtgärda detta problem sätt värdet mbstring.func_overload till 0 i din php.ini", "libxml2 2.7.0 is at least required. Currently %s is installed." : "libxml2 2.7.0 är det minsta som krävs. För närvarande är %s installerat.", "To fix this issue update your libxml2 version and restart your web server." : "För att åtgärda detta problem uppdatera libxml2 versionen och starta om din webbserver.", "PHP is apparently set up to strip inline doc blocks. This will make several core apps inaccessible." : "PHP är tydligen inställt för att tömma \"inline doc blocks\". Detta kommer att göra flera kärnprogram otillgängliga.", "This is probably caused by a cache/accelerator such as Zend OPcache or eAccelerator." : "Detta orsakas troligtvis av en cache/accelerator som t ex Zend OPchache eller eAccelerator.", "PHP modules have been installed, but they are still listed as missing?" : "PHP-moduler har installerats, men de listas fortfarande som saknade?", "Please ask your server administrator to restart the web server." : "Vänligen be din serveradministratör att starta om webbservern.", "Please upgrade your database version" : "Vänligen uppgradera din databas-version", "Please change the permissions to 0770 so that the directory cannot be listed by other users." : "Vänligen ändra rättigheterna till 0770 så att katalogen inte kan listas av andra användare.", "Your data directory is readable by other users" : "Din datamapp är läsbar av andra användare", "Your data directory must be an absolute path" : "Du måste specificera en korrekt sökväg till datamappen", "Check the value of \"datadirectory\" in your configuration" : "Kontrollera värdet av \"datakatalog\" i din konfiguration", "Your data directory is invalid" : "Din datamapp är ogiltig", "Ensure there is a file called \".ocdata\" in the root of the data directory." : "Säkerställ att du har filen \".ocdata\" i huvudkatalogen för din data.", "Action \"%s\" not supported or implemented." : "Åtgärd \"%s\" stöds ej eller är inte implementerad.", "Authentication failed, wrong token or provider ID given" : "Autentisering misslyckades, felaktig token eller leverantörs-ID", "Parameters missing in order to complete the request. Missing Parameters: \"%s\"" : "Parametrar saknas för att slutföra förfrågan. Saknade parametrar: \"%s\"", "ID \"%1$s\" already used by cloud federation provider \"%2$s\"" : "ID \"%1$s\" används redan av moln - federationsleverantör \"%2$s\"", "Cloud Federation Provider with ID: \"%s\" does not exist." : "Moln - federationsleverantör med ID: \"%s\" finns inte.", "Could not obtain lock type %d on \"%s\"." : "Kunde inte hämta låstyp %d på \"%s\".", "Storage unauthorized. %s" : "Lagringsutrymme ej tillåtet. %s", "Storage incomplete configuration. %s" : "Lagringsutrymme felaktigt inställt. %s", "Storage connection error. %s" : "Lagringsutrymme lyckas inte ansluta. %s", "Storage is temporarily not available" : "Lagringsutrymme är för tillfället inte tillgängligt", "Storage connection timeout. %s" : "Lagringsutrymme lyckas inte ansluta \"timeout\". %s", "Library %s with a version higher than %s is required - available version %s." : "Bibliotek %s med version högre än %s krävs - tillgänglig version %s.", "Library %s with a version lower than %s is required - available version %s." : "Bibliotek %s med version lägre än %s krävs - tillgänglig version %s.", "Create" : "Skapa", "Change" : "Ändra", "Delete" : "Radera", "Share" : "Dela", "Unlimited" : "Obegränsad", "Verifying" : "Verifierar", "Verifying …" : "Verifierar ...", "Verify" : "Verifiera", "Sharing %s failed, because the backend does not allow shares from type %i" : "Misslyckades dela ut %s då backend inte tillåter delningar från typ %i", "Sharing %s failed, because the file does not exist" : "Delning av %s misslyckades på grund av att filen inte existerar", "Sharing %s failed, because you can not share with yourself" : "Delning %s misslyckades därför att du inte kan dela med dig själv.", "Sharing %s failed, because the user %s does not exist" : "Delning %s misslyckades därför att användaren %s inte existerar", "Sharing %s failed, because the user %s is not a member of any groups that %s is a member of" : "Delning %s misslyckades därför att användaren %s inte är medlem i någon av de grupper som %s är medlem i", "Sharing %s failed, because this item is already shared with %s" : "Delning %s misslyckades därför att objektet redan är delat med %s", "Sharing %s failed, because this item is already shared with user %s" : "Delning %s misslyckades därför att detta redan är delat med användaren %s", "Sharing %s failed, because the group %s does not exist" : "Delning %s misslyckades därför att gruppen %s inte existerar", "Sharing %s failed, because %s is not a member of the group %s" : "Delning %s misslyckades därför att %s inte ingår i gruppen %s", "You need to provide a password to create a public link, only protected links are allowed" : "Du måste ange ett lösenord för att skapa en offentlig länk, endast skyddade länkar är tillåtna", "Sharing %s failed, because sharing with links is not allowed" : "Delning %s misslyckades därför att delning av länkar inte är tillåtet", "Not allowed to create a federated share with the same user" : "Ej tillåtet att skapa en federerad delning med samma användare", "Sharing %s failed, could not find %s, maybe the server is currently unreachable." : "Misslyckades dela ut %s, kan inte hitta %s, kanske är servern inte åtkomlig för närvarande.", "Share type %s is not valid for %s" : "Delningstyp %s är inte giltig för %s", "Cannot set expiration date. Shares cannot expire later than %s after they have been shared" : "Kan inte sätta utgångsdatum. Utdelningar kan inte utgå senare än %s efter de har delats ut", "Cannot set expiration date. Expiration date is in the past" : "Kan inte sätta utgångsdatum. Utgångsdatumet är i det förflutna.", "Sharing failed, because the user %s is the original sharer" : "Delning misslyckades eftersom användaren %s redan är den som har delat detta.", "Sharing %s failed, because the permissions exceed permissions granted to %s" : "Delning %s misslyckades därför att rättigheterna överskrider de rättigheter som är tillåtna för %s", "Sharing %s failed, because resharing is not allowed" : "Delning %s misslyckades därför att vidaredelning inte är tillåten", "Sharing %s failed, because the sharing backend for %s could not find its source" : "Delning %s misslyckades därför att delningsgränsnittet för %s inte kunde hitta sin källa", "Sharing %s failed, because the file could not be found in the file cache" : "Delning %s misslyckades därför att filen inte kunde hittas i filcachen", "%s shared »%s« with you" : "%s delade »%s« med dig", "%s shared »%s« with you." : "%s delade »%s« med dig.", "%s via %s" : "%s via %s", "App \"%s\" cannot be installed because the following dependencies are not fulfilled: %s" : "Applikationen \"%s\" kan ej installeras eftersom följande kriterier inte är uppfyllda: %s", "PostgreSQL >= 9 required" : "PostgreSQL >= 9 krävs", "ID \"%s\" already used by cloud federation provider \"%s\"" : "ID \"%s\" används redan av moln - federationsleverantör \"%s\"", "Sharing %1$s failed, because the user %2$s does not exist" : "Delning %1$s misslyckades för att användaren %2$s inte finns", "Sharing %1$s failed, because the user %2$s is not a member of any groups that %3$s is a member of" : "Delning %1$s misslyckades för att användaren %2$s inte är medlem i några grupper som %3$s är medlem i", "Sharing %1$s failed, because this item is already shared with %2$s" : "Delning %1$s misslyckades, är redan delad med %2$s", "Sharing %1$s failed, because this item is already shared with user %2$s" : "Delning %1$s misslyckades, är redan delad med användare %2$s", "Sharing %1$s failed, because the group %2$s does not exist" : "Delning %1$s misslyckades för att gruppen %2$s finns inte", "Sharing %1$s failed, because %2$s is not a member of the group %3$s" : "Delning %1$s misslyckades för att %2$s är inte medlem i gruppen %3$s", "Sharing %1$s failed, could not find %2$s, maybe the server is currently unreachable." : "Delning %1$s misslyckades, kan inte hitta %2$s, servern är kanske inte åtkomlig för närvarande.", "Share type %1$s is not valid for %2$s" : "Delningstyp %1$s är inte giltig för %2$s", "Sharing %1$s failed, because the permissions exceed permissions granted to %2$s" : "Delning %1$s misslyckades därför att rättigheterna överskrider de rättigheter som tilldelats %2$s", "Sharing %1$s failed, because the sharing backend for %2$s could not find its source" : "Delning %1$s misslyckades därför att delningsgränssnittet för %2$s inte kunde hitta sin källa" }, "nplurals=2; plural=(n != 1);"); 5'>615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package repo

import (
	"bufio"
	gocontext "context"
	"encoding/csv"
	"errors"
	"fmt"
	"html"
	"io"
	"net/http"
	"net/url"
	"path/filepath"
	"strings"

	git_model "code.gitea.io/gitea/models/git"
	issues_model "code.gitea.io/gitea/models/issues"
	access_model "code.gitea.io/gitea/models/perm/access"
	repo_model "code.gitea.io/gitea/models/repo"
	"code.gitea.io/gitea/models/unit"
	user_model "code.gitea.io/gitea/models/user"
	"code.gitea.io/gitea/modules/base"
	"code.gitea.io/gitea/modules/charset"
	"code.gitea.io/gitea/modules/context"
	csv_module "code.gitea.io/gitea/modules/csv"
	"code.gitea.io/gitea/modules/git"
	"code.gitea.io/gitea/modules/log"
	"code.gitea.io/gitea/modules/markup"
	"code.gitea.io/gitea/modules/setting"
	api "code.gitea.io/gitea/modules/structs"
	"code.gitea.io/gitea/modules/upload"
	"code.gitea.io/gitea/modules/util"
	"code.gitea.io/gitea/services/gitdiff"
)

const (
	tplCompare     base.TplName = "repo/diff/compare"
	tplBlobExcerpt base.TplName = "repo/diff/blob_excerpt"
	tplDiffBox     base.TplName = "repo/diff/box"
)

// setCompareContext sets context data.
func setCompareContext(ctx *context.Context, base, head *git.Commit, headOwner, headName string) {
	ctx.Data["BaseCommit"] = base
	ctx.Data["HeadCommit"] = head

	ctx.Data["GetBlobByPathForCommit"] = func(commit *git.Commit, path string) *git.Blob {
		if commit == nil {
			return nil
		}

		blob, err := commit.GetBlobByPath(path)
		if err != nil {
			return nil
		}
		return blob
	}

	setPathsCompareContext(ctx, base, head, headOwner, headName)
	setImageCompareContext(ctx)
	setCsvCompareContext(ctx)
}

// SourceCommitURL creates a relative URL for a commit in the given repository
func SourceCommitURL(owner, name string, commit *git.Commit) string {
	return setting.AppSubURL + "/" + url.PathEscape(owner) + "/" + url.PathEscape(name) + "/src/commit/" + url.PathEscape(commit.ID.String())
}

// RawCommitURL creates a relative URL for the raw commit in the given repository
func RawCommitURL(owner, name string, commit *git.Commit) string {
	return setting.AppSubURL + "/" + url.PathEscape(owner) + "/" + url.PathEscape(name) + "/raw/commit/" + url.PathEscape(commit.ID.String())
}

// setPathsCompareContext sets context data for source and raw paths
func setPathsCompareContext(ctx *context.Context, base, head *git.Commit, headOwner, headName string) {
	ctx.Data["SourcePath"] = SourceCommitURL(headOwner, headName, head)
	ctx.Data["RawPath"] = RawCommitURL(headOwner, headName, head)
	if base != nil {
		ctx.Data["BeforeSourcePath"] = SourceCommitURL(headOwner, headName, base)
		ctx.Data["BeforeRawPath"] = RawCommitURL(headOwner, headName, base)
	}
}

// setImageCompareContext sets context data that is required by image compare template
func setImageCompareContext(ctx *context.Context) {
	ctx.Data["IsBlobAnImage"] = func(blob *git.Blob) bool {
		if blob == nil {
			return false
		}

		st, err := blob.GuessContentType()
		if err != nil {
			log.Error("GuessContentType failed: %v", err)
			return false
		}
		return st.IsImage() && (setting.UI.SVG.Enabled || !st.IsSvgImage())
	}
}

// setCsvCompareContext sets context data that is required by the CSV compare template
func setCsvCompareContext(ctx *context.Context) {
	ctx.Data["IsCsvFile"] = func(diffFile *gitdiff.DiffFile) bool {
		extension := strings.ToLower(filepath.Ext(diffFile.Name))
		return extension == ".csv" || extension == ".tsv"
	}

	type CsvDiffResult struct {
		Sections []*gitdiff.TableDiffSection
		Error    string
	}

	ctx.Data["CreateCsvDiff"] = func(diffFile *gitdiff.DiffFile, baseBlob, headBlob *git.Blob) CsvDiffResult {
		if diffFile == nil {
			return CsvDiffResult{nil, ""}
		}

		errTooLarge := errors.New(ctx.Locale.Tr("repo.error.csv.too_large"))

		csvReaderFromCommit := func(ctx *markup.RenderContext, blob *git.Blob) (*csv.Reader, io.Closer, error) {
			if blob == nil {
				// It's ok for blob to be nil (file added or deleted)
				return nil, nil, nil
			}

			if setting.UI.CSV.MaxFileSize != 0 && setting.UI.CSV.MaxFileSize < blob.Size() {
				return nil, nil, errTooLarge
			}

			reader, err := blob.DataAsync()
			if err != nil {
				return nil, nil, err
			}

			csvReader, err := csv_module.CreateReaderAndDetermineDelimiter(ctx, charset.ToUTF8WithFallbackReader(reader))
			return csvReader, reader, err
		}

		baseReader, baseBlobCloser, err := csvReaderFromCommit(&markup.RenderContext{Ctx: ctx, RelativePath: diffFile.OldName}, baseBlob)
		if baseBlobCloser != nil {
			defer baseBlobCloser.Close()
		}
		if err != nil {
			if err == errTooLarge {
				return CsvDiffResult{nil, err.Error()}
			}
			log.Error("error whilst creating csv.Reader from file %s in base commit %s in %s: %v", diffFile.Name, baseBlob.ID.String(), ctx.Repo.Repository.Name, err)
			return CsvDiffResult{nil, "unable to load file"}
		}

		headReader, headBlobCloser, err := csvReaderFromCommit(&markup.RenderContext{Ctx: ctx, RelativePath: diffFile.Name}, headBlob)
		if headBlobCloser != nil {
			defer headBlobCloser.Close()
		}
		if err != nil {
			if err == errTooLarge {
				return CsvDiffResult{nil, err.Error()}
			}
			log.Error("error whilst creating csv.Reader from file %s in head commit %s in %s: %v", diffFile.Name, headBlob.ID.String(), ctx.Repo.Repository.Name, err)
			return CsvDiffResult{nil, "unable to load file"}
		}

		sections, err := gitdiff.CreateCsvDiff(diffFile, baseReader, headReader)
		if err != nil {
			errMessage, err := csv_module.FormatError(err, ctx.Locale)
			if err != nil {
				log.Error("CreateCsvDiff FormatError failed: %v", err)
				return CsvDiffResult{nil, "unknown csv diff error"}
			}
			return CsvDiffResult{nil, errMessage}
		}
		return CsvDiffResult{sections, ""}
	}
}

// CompareInfo represents the collected results from ParseCompareInfo
type CompareInfo struct {
	HeadUser         *user_model.User
	HeadRepo         *repo_model.Repository
	HeadGitRepo      *git.Repository
	CompareInfo      *git.CompareInfo
	BaseBranch       string
	HeadBranch       string
	DirectComparison bool
}

// ParseCompareInfo parse compare info between two commit for preparing comparing references
func ParseCompareInfo(ctx *context.Context) *CompareInfo {
	baseRepo := ctx.Repo.Repository
	ci := &CompareInfo{}

	fileOnly := ctx.FormBool("file-only")

	// Get compared branches information
	// A full compare url is of the form:
	//
	// 1. /{:baseOwner}/{:baseRepoName}/compare/{:baseBranch}...{:headBranch}
	// 2. /{:baseOwner}/{:baseRepoName}/compare/{:baseBranch}...{:headOwner}:{:headBranch}
	// 3. /{:baseOwner}/{:baseRepoName}/compare/{:baseBranch}...{:headOwner}/{:headRepoName}:{:headBranch}
	// 4. /{:baseOwner}/{:baseRepoName}/compare/{:headBranch}
	// 5. /{:baseOwner}/{:baseRepoName}/compare/{:headOwner}:{:headBranch}
	// 6. /{:baseOwner}/{:baseRepoName}/compare/{:headOwner}/{:headRepoName}:{:headBranch}
	//
	// Here we obtain the infoPath "{:baseBranch}...[{:headOwner}/{:headRepoName}:]{:headBranch}" as ctx.Params("*")
	// with the :baseRepo in ctx.Repo.
	//
	// Note: Generally :headRepoName is not provided here - we are only passed :headOwner.
	//
	// How do we determine the :headRepo?
	//
	// 1. If :headOwner is not set then the :headRepo = :baseRepo
	// 2. If :headOwner is set - then look for the fork of :baseRepo owned by :headOwner
	// 3. But... :baseRepo could be a fork of :headOwner's repo - so check that
	// 4. Now, :baseRepo and :headRepos could be forks of the same repo - so check that
	//
	// format: <base branch>...[<head repo>:]<head branch>
	// base<-head: master...head:feature
	// same repo: master...feature

	var (
		isSameRepo bool
		infoPath   string
		err        error
	)

	infoPath = ctx.Params("*")
	var infos []string
	if infoPath == "" {
		infos = []string{baseRepo.DefaultBranch, baseRepo.DefaultBranch}
	} else {
		infos = strings.SplitN(infoPath, "...", 2)
		if len(infos) != 2 {
			if infos = strings.SplitN(infoPath, "..", 2); len(infos) == 2 {
				ci.DirectComparison = true
				ctx.Data["PageIsComparePull"] = false
			} else {
				infos = []string{baseRepo.DefaultBranch, infoPath}
			}
		}
	}

	ctx.Data["BaseName"] = baseRepo.OwnerName
	ci.BaseBranch = infos[0]
	ctx.Data["BaseBranch"] = ci.BaseBranch

	// If there is no head repository, it means compare between same repository.
	headInfos := strings.Split(infos[1], ":")
	if len(headInfos) == 1 {
		isSameRepo = true
		ci.HeadUser = ctx.Repo.Owner
		ci.HeadBranch = headInfos[0]

	} else if len(headInfos) == 2 {
		headInfosSplit := strings.Split(headInfos[0], "/")
		if len(headInfosSplit) == 1 {
			ci.HeadUser, err = user_model.GetUserByName(ctx, headInfos[0])
			if err != nil {
				if user_model.IsErrUserNotExist(err) {
					ctx.NotFound("GetUserByName", nil)
				} else {
					ctx.ServerError("GetUserByName", err)
				}
				return nil
			}
			ci.HeadBranch = headInfos[1]
			isSameRepo = ci.HeadUser.ID == ctx.Repo.Owner.ID
			if isSameRepo {
				ci.HeadRepo = baseRepo
			}
		} else {
			ci.HeadRepo, err = repo_model.GetRepositoryByOwnerAndName(ctx, headInfosSplit[0], headInfosSplit[1])
			if err != nil {
				if repo_model.IsErrRepoNotExist(err) {
					ctx.NotFound("GetRepositoryByOwnerAndName", nil)
				} else {
					ctx.ServerError("GetRepositoryByOwnerAndName", err)
				}
				return nil
			}
			if err := ci.HeadRepo.GetOwner(ctx); err != nil {
				if user_model.IsErrUserNotExist(err) {
					ctx.NotFound("GetUserByName", nil)
				} else {
					ctx.ServerError("GetUserByName", err)
				}
				return nil
			}
			ci.HeadBranch = headInfos[1]
			ci.HeadUser = ci.HeadRepo.Owner
			isSameRepo = ci.HeadRepo.ID == ctx.Repo.Repository.ID
		}
	} else {
		ctx.NotFound("CompareAndPullRequest", nil)
		return nil
	}
	ctx.Data["HeadUser"] = ci.HeadUser
	ctx.Data["HeadBranch"] = ci.HeadBranch
	ctx.Repo.PullRequest.SameRepo = isSameRepo

	// Check if base branch is valid.
	baseIsCommit := ctx.Repo.GitRepo.IsCommitExist(ci.BaseBranch)
	baseIsBranch := ctx.Repo.GitRepo.IsBranchExist(ci.BaseBranch)
	baseIsTag := ctx.Repo.GitRepo.IsTagExist(ci.BaseBranch)
	if !baseIsCommit && !baseIsBranch && !baseIsTag {
		// Check if baseBranch is short sha commit hash
		if baseCommit, _ := ctx.Repo.GitRepo.GetCommit(ci.BaseBranch); baseCommit != nil {
			ci.BaseBranch = baseCommit.ID.String()
			ctx.Data["BaseBranch"] = ci.BaseBranch
			baseIsCommit = true
		} else if ci.BaseBranch == git.EmptySHA {
			if isSameRepo {
				ctx.Redirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(ci.HeadBranch))
			} else {
				ctx.Redirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(ci.HeadRepo.FullName()) + ":" + util.PathEscapeSegments(ci.HeadBranch))
			}
			return nil
		} else {
			ctx.NotFound("IsRefExist", nil)
			return nil
		}
	}
	ctx.Data["BaseIsCommit"] = baseIsCommit
	ctx.Data["BaseIsBranch"] = baseIsBranch
	ctx.Data["BaseIsTag"] = baseIsTag
	ctx.Data["IsPull"] = true

	// Now we have the repository that represents the base

	// The current base and head repositories and branches may not
	// actually be the intended branches that the user wants to
	// create a pull-request from - but also determining the head
	// repo is difficult.

	// We will want therefore to offer a few repositories to set as
	// our base and head

	// 1. First if the baseRepo is a fork get the "RootRepo" it was
	// forked from
	var rootRepo *repo_model.Repository
	if baseRepo.IsFork {
		err = baseRepo.GetBaseRepo(ctx)
		if err != nil {
			if !repo_model.IsErrRepoNotExist(err) {
				ctx.ServerError("Unable to find root repo", err)
				return nil
			}
		} else {
			rootRepo = baseRepo.BaseRepo
		}
	}

	// 2. Now if the current user is not the owner of the baseRepo,
	// check if they have a fork of the base repo and offer that as
	// "OwnForkRepo"
	var ownForkRepo *repo_model.Repository
	if ctx.Doer != nil && baseRepo.OwnerID != ctx.Doer.ID {
		repo := repo_model.GetForkedRepo(ctx.Doer.ID, baseRepo.ID)
		if repo != nil {
			ownForkRepo = repo
			ctx.Data["OwnForkRepo"] = ownForkRepo
		}
	}

	has := ci.HeadRepo != nil
	// 3. If the base is a forked from "RootRepo" and the owner of
	// the "RootRepo" is the :headUser - set headRepo to that
	if !has && rootRepo != nil && rootRepo.OwnerID == ci.HeadUser.ID {
		ci.HeadRepo = rootRepo
		has = true
	}

	// 4. If the ctx.Doer has their own fork of the baseRepo and the headUser is the ctx.Doer
	// set the headRepo to the ownFork
	if !has && ownForkRepo != nil && ownForkRepo.OwnerID == ci.HeadUser.ID {
		ci.HeadRepo = ownForkRepo
		has = true
	}

	// 5. If the headOwner has a fork of the baseRepo - use that
	if !has {
		ci.HeadRepo = repo_model.GetForkedRepo(ci.HeadUser.ID, baseRepo.ID)
		has = ci.HeadRepo != nil
	}

	// 6. If the baseRepo is a fork and the headUser has a fork of that use that
	if !has && baseRepo.IsFork {
		ci.HeadRepo = repo_model.GetForkedRepo(ci.HeadUser.ID, baseRepo.ForkID)
		has = ci.HeadRepo != nil
	}

	// 7. Otherwise if we're not the same repo and haven't found a repo give up
	if !isSameRepo && !has {
		ctx.Data["PageIsComparePull"] = false
	}

	// 8. Finally open the git repo
	if isSameRepo {
		ci.HeadRepo = ctx.Repo.Repository
		ci.HeadGitRepo = ctx.Repo.GitRepo
	} else if has {
		ci.HeadGitRepo, err = git.OpenRepository(ctx, ci.HeadRepo.RepoPath())
		if err != nil {
			ctx.ServerError("OpenRepository", err)
			return nil
		}
		defer ci.HeadGitRepo.Close()
	}

	ctx.Data["HeadRepo"] = ci.HeadRepo
	ctx.Data["BaseCompareRepo"] = ctx.Repo.Repository

	// Now we need to assert that the ctx.Doer has permission to read
	// the baseRepo's code and pulls
	// (NOT headRepo's)
	permBase, err := access_model.GetUserRepoPermission(ctx, baseRepo, ctx.Doer)
	if err != nil {
		ctx.ServerError("GetUserRepoPermission", err)
		return nil
	}
	if !permBase.CanRead(unit.TypeCode) {
		if log.IsTrace() {
			log.Trace("Permission Denied: User: %-v cannot read code in Repo: %-v\nUser in baseRepo has Permissions: %-+v",
				ctx.Doer,
				baseRepo,
				permBase)
		}
		ctx.NotFound("ParseCompareInfo", nil)
		return nil
	}

	// If we're not merging from the same repo:
	if !isSameRepo {
		// Assert ctx.Doer has permission to read headRepo's codes
		permHead, err := access_model.GetUserRepoPermission(ctx, ci.HeadRepo, ctx.Doer)
		if err != nil {
			ctx.ServerError("GetUserRepoPermission", err)
			return nil
		}
		if !permHead.CanRead(unit.TypeCode) {
			if log.IsTrace() {
				log.Trace("Permission Denied: User: %-v cannot read code in Repo: %-v\nUser in headRepo has Permissions: %-+v",
					ctx.Doer,
					ci.HeadRepo,
					permHead)
			}
			ctx.NotFound("ParseCompareInfo", nil)
			return nil
		}
		ctx.Data["CanWriteToHeadRepo"] = permHead.CanWrite(unit.TypeCode)
	}

	// If we have a rootRepo and it's different from:
	// 1. the computed base
	// 2. the computed head
	// then get the branches of it
	if rootRepo != nil &&
		rootRepo.ID != ci.HeadRepo.ID &&
		rootRepo.ID != baseRepo.ID {
		canRead := access_model.CheckRepoUnitUser(ctx, rootRepo, ctx.Doer, unit.TypeCode)
		if canRead {
			ctx.Data["RootRepo"] = rootRepo
			if !fileOnly {
				branches, tags, err := getBranchesAndTagsForRepo(ctx, rootRepo)
				if err != nil {
					ctx.ServerError("GetBranchesForRepo", err)
					return nil
				}

				ctx.Data["RootRepoBranches"] = branches
				ctx.Data["RootRepoTags"] = tags
			}
		}
	}

	// If we have a ownForkRepo and it's different from:
	// 1. The computed base
	// 2. The computed head
	// 3. The rootRepo (if we have one)
	// then get the branches from it.
	if ownForkRepo != nil &&
		ownForkRepo.ID != ci.HeadRepo.ID &&
		ownForkRepo.ID != baseRepo.ID &&
		(rootRepo == nil || ownForkRepo.ID != rootRepo.ID) {
		canRead := access_model.CheckRepoUnitUser(ctx, ownForkRepo, ctx.Doer, unit.TypeCode)
		if canRead {
			ctx.Data["OwnForkRepo"] = ownForkRepo
			if !fileOnly {
				branches, tags, err := getBranchesAndTagsForRepo(ctx, ownForkRepo)
				if err != nil {
					ctx.ServerError("GetBranchesForRepo", err)
					return nil
				}
				ctx.Data["OwnForkRepoBranches"] = branches
				ctx.Data["OwnForkRepoTags"] = tags
			}
		}
	}

	// Check if head branch is valid.
	headIsCommit := ci.HeadGitRepo.IsCommitExist(ci.HeadBranch)
	headIsBranch := ci.HeadGitRepo.IsBranchExist(ci.HeadBranch)
	headIsTag := ci.HeadGitRepo.IsTagExist(ci.HeadBranch)
	if !headIsCommit && !headIsBranch && !headIsTag {
		// Check if headBranch is short sha commit hash
		if headCommit, _ := ci.HeadGitRepo.GetCommit(ci.HeadBranch); headCommit != nil {
			ci.HeadBranch = headCommit.ID.String()
			ctx.Data["HeadBranch"] = ci.HeadBranch
			headIsCommit = true
		} else {
			ctx.NotFound("IsRefExist", nil)
			return nil
		}
	}
	ctx.Data["HeadIsCommit"] = headIsCommit
	ctx.Data["HeadIsBranch"] = headIsBranch
	ctx.Data["HeadIsTag"] = headIsTag

	// Treat as pull request if both references are branches
	if ctx.Data["PageIsComparePull"] == nil {
		ctx.Data["PageIsComparePull"] = headIsBranch && baseIsBranch
	}

	if ctx.Data["PageIsComparePull"] == true && !permBase.CanReadIssuesOrPulls(true) {
		if log.IsTrace() {
			log.Trace("Permission Denied: User: %-v cannot create/read pull requests in Repo: %-v\nUser in baseRepo has Permissions: %-+v",
				ctx.Doer,
				baseRepo,
				permBase)
		}
		ctx.NotFound("ParseCompareInfo", nil)
		return nil
	}

	baseBranchRef := ci.BaseBranch
	if baseIsBranch {
		baseBranchRef = git.BranchPrefix + ci.BaseBranch
	} else if baseIsTag {
		baseBranchRef = git.TagPrefix + ci.BaseBranch
	}
	headBranchRef := ci.HeadBranch
	if headIsBranch {
		headBranchRef = git.BranchPrefix + ci.HeadBranch
	} else if headIsTag {
		headBranchRef = git.TagPrefix + ci.HeadBranch
	}

	ci.CompareInfo, err = ci.HeadGitRepo.GetCompareInfo(baseRepo.RepoPath(), baseBranchRef, headBranchRef, ci.DirectComparison, fileOnly)
	if err != nil {
		ctx.ServerError("GetCompareInfo", err)
		return nil
	}
	ctx.Data["BeforeCommitID"] = ci.CompareInfo.MergeBase

	return ci
}

// PrepareCompareDiff renders compare diff page
func PrepareCompareDiff(
	ctx *context.Context,
	ci *CompareInfo,
	whitespaceBehavior git.TrustedCmdArgs,
) bool {
	var (
		repo  = ctx.Repo.Repository
		err   error
		title string
	)

	// Get diff information.
	ctx.Data["CommitRepoLink"] = ci.HeadRepo.Link()

	headCommitID := ci.CompareInfo.HeadCommitID

	ctx.Data["AfterCommitID"] = headCommitID

	if (headCommitID == ci.CompareInfo.MergeBase && !ci.DirectComparison) ||
		headCommitID == ci.CompareInfo.BaseCommitID {
		ctx.Data["IsNothingToCompare"] = true
		if unit, err := repo.GetUnit(ctx, unit.TypePullRequests); err == nil {
			config := unit.PullRequestsConfig()

			if !config.AutodetectManualMerge {
				allowEmptyPr := !(ci.BaseBranch == ci.HeadBranch && ctx.Repo.Repository.Name == ci.HeadRepo.Name)
				ctx.Data["AllowEmptyPr"] = allowEmptyPr

				return !allowEmptyPr
			}

			ctx.Data["AllowEmptyPr"] = false
		}
		return true
	}

	beforeCommitID := ci.CompareInfo.MergeBase
	if ci.DirectComparison {
		beforeCommitID = ci.CompareInfo.BaseCommitID
	}

	maxLines, maxFiles := setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffFiles
	files := ctx.FormStrings("files")
	if len(files) == 2 || len(files) == 1 {
		maxLines, maxFiles = -1, -1
	}

	diff, err := gitdiff.GetDiff(ci.HeadGitRepo,
		&gitdiff.DiffOptions{
			BeforeCommitID:     beforeCommitID,
			AfterCommitID:      headCommitID,
			SkipTo:             ctx.FormString("skip-to"),
			MaxLines:           maxLines,
			MaxLineCharacters:  setting.Git.MaxGitDiffLineCharacters,
			MaxFiles:           maxFiles,
			WhitespaceBehavior: whitespaceBehavior,
			DirectComparison:   ci.DirectComparison,
		}, ctx.FormStrings("files")...)
	if err != nil {
		ctx.ServerError("GetDiffRangeWithWhitespaceBehavior", err)
		return false
	}
	ctx.Data["Diff"] = diff
	ctx.Data["DiffNotAvailable"] = diff.NumFiles == 0

	headCommit, err := ci.HeadGitRepo.GetCommit(headCommitID)
	if err != nil {
		ctx.ServerError("GetCommit", err)
		return false
	}

	baseGitRepo := ctx.Repo.GitRepo
	baseCommitID := ci.CompareInfo.BaseCommitID

	baseCommit, err := baseGitRepo.GetCommit(baseCommitID)
	if err != nil {
		ctx.ServerError("GetCommit", err)
		return false
	}

	commits := git_model.ConvertFromGitCommit(ctx, ci.CompareInfo.Commits, ci.HeadRepo)
	ctx.Data["Commits"] = commits
	ctx.Data["CommitCount"] = len(commits)

	if len(commits) == 1 {
		c := commits[0]
		title = strings.TrimSpace(c.UserCommit.Summary())

		body := strings.Split(strings.TrimSpace(c.UserCommit.Message()), "\n")
		if len(body) > 1 {
			ctx.Data["content"] = strings.Join(body[1:], "\n")
		}
	} else {
		title = ci.HeadBranch
	}
	if len(title) > 255 {
		var trailer string
		title, trailer = util.SplitStringAtByteN(title, 255)
		if len(trailer) > 0 {
			if ctx.Data["content"] != nil {
				ctx.Data["content"] = fmt.Sprintf("%s\n\n%s", trailer, ctx.Data["content"])
			} else {
				ctx.Data["content"] = trailer + "\n"
			}
		}
	}

	ctx.Data["title"] = title
	ctx.Data["Username"] = ci.HeadUser.Name
	ctx.Data["Reponame"] = ci.HeadRepo.Name

	setCompareContext(ctx, baseCommit, headCommit, ci.HeadUser.Name, repo.Name)

	return false
}

func getBranchesAndTagsForRepo(ctx gocontext.Context, repo *repo_model.Repository) (branches, tags []string, err error) {
	gitRepo, err := git.OpenRepository(ctx, repo.RepoPath())
	if err != nil {
		return nil, nil, err
	}
	defer gitRepo.Close()

	branches, _, err = gitRepo.GetBranchNames(0, 0)
	if err != nil {
		return nil, nil, err
	}
	tags, err = gitRepo.GetTags(0, 0)
	if err != nil {
		return nil, nil, err
	}
	return branches, tags, nil
}

// CompareDiff show different from one commit to another commit
func CompareDiff(ctx *context.Context) {
	ci := ParseCompareInfo(ctx)
	defer func() {
		if ci != nil && ci.HeadGitRepo != nil {
			ci.HeadGitRepo.Close()
		}
	}()
	if ctx.Written() {
		return
	}

	ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
	ctx.Data["DirectComparison"] = ci.DirectComparison
	ctx.Data["OtherCompareSeparator"] = ".."
	ctx.Data["CompareSeparator"] = "..."
	if ci.DirectComparison {
		ctx.Data["CompareSeparator"] = ".."
		ctx.Data["OtherCompareSeparator"] = "..."
	}

	nothingToCompare := PrepareCompareDiff(ctx, ci,
		gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string)))
	if ctx.Written() {
		return
	}

	baseGitRepo := ctx.Repo.GitRepo
	baseTags, err := baseGitRepo.GetTags(0, 0)
	if err != nil {
		ctx.ServerError("GetTags", err)
		return
	}
	ctx.Data["Tags"] = baseTags

	fileOnly := ctx.FormBool("file-only")
	if fileOnly {
		ctx.HTML(http.StatusOK, tplDiffBox)
		return
	}

	headBranches, _, err := ci.HeadGitRepo.GetBranchNames(0, 0)
	if err != nil {
		ctx.ServerError("GetBranches", err)
		return
	}
	ctx.Data["HeadBranches"] = headBranches

	headTags, err := ci.HeadGitRepo.GetTags(0, 0)
	if err != nil {
		ctx.ServerError("GetTags", err)
		return
	}
	ctx.Data["HeadTags"] = headTags

	if ctx.Data["PageIsComparePull"] == true {
		pr, err := issues_model.GetUnmergedPullRequest(ctx, ci.HeadRepo.ID, ctx.Repo.Repository.ID, ci.HeadBranch, ci.BaseBranch, issues_model.PullRequestFlowGithub)
		if err != nil {
			if !issues_model.IsErrPullRequestNotExist(err) {
				ctx.ServerError("GetUnmergedPullRequest", err)
				return
			}
		} else {
			ctx.Data["HasPullRequest"] = true
			if err := pr.LoadIssue(ctx); err != nil {
				ctx.ServerError("LoadIssue", err)
				return
			}
			ctx.Data["PullRequest"] = pr
			ctx.HTML(http.StatusOK, tplCompareDiff)
			return
		}

		if !nothingToCompare {
			// Setup information for new form.
			RetrieveRepoMetas(ctx, ctx.Repo.Repository, true)
			if ctx.Written() {
				return
			}
		}
	}
	beforeCommitID := ctx.Data["BeforeCommitID"].(string)
	afterCommitID := ctx.Data["AfterCommitID"].(string)

	separator := "..."
	if ci.DirectComparison {
		separator = ".."
	}
	ctx.Data["Title"] = "Comparing " + base.ShortSha(beforeCommitID) + separator + base.ShortSha(afterCommitID)

	ctx.Data["IsRepoToolbarCommits"] = true
	ctx.Data["IsDiffCompare"] = true
	ctx.Data["RequireTribute"] = true
	templateErrs := setTemplateIfExists(ctx, pullRequestTemplateKey, pullRequestTemplateCandidates)

	if len(templateErrs) > 0 {
		ctx.Flash.Warning(renderErrorOfTemplates(ctx, templateErrs), true)
	}

	if content, ok := ctx.Data["content"].(string); ok && content != "" {
		// If a template content is set, prepend the "content". In this case that's only
		// applicable if you have one commit to compare and that commit has a message.
		// In that case the commit message will be prepend to the template body.
		if templateContent, ok := ctx.Data[pullRequestTemplateKey].(string); ok && templateContent != "" {
			// Re-use the same key as that's priortized over the "content" key.
			// Add two new lines between the content to ensure there's always at least
			// one empty line between them.
			ctx.Data[pullRequestTemplateKey] = content + "\n\n" + templateContent
		}

		// When using form fields, also add content to field with id "body".
		if fields, ok := ctx.Data["Fields"].([]*api.IssueFormField); ok {
			for _, field := range fields {
				if field.ID == "body" {
					if fieldValue, ok := field.Attributes["value"].(string); ok && fieldValue != "" {
						field.Attributes["value"] = content + "\n\n" + fieldValue
					} else {
						field.Attributes["value"] = content
					}
				}
			}
		}
	}

	ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
	upload.AddUploadContext(ctx, "comment")

	ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWrite(unit.TypePullRequests)

	if unit, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypePullRequests); err == nil {
		config := unit.PullRequestsConfig()
		ctx.Data["AllowMaintainerEdit"] = config.DefaultAllowMaintainerEdit
	} else {
		ctx.Data["AllowMaintainerEdit"] = false
	}

	ctx.HTML(http.StatusOK, tplCompare)
}

// ExcerptBlob render blob excerpt contents
func ExcerptBlob(ctx *context.Context) {
	commitID := ctx.Params("sha")
	lastLeft := ctx.FormInt("last_left")
	lastRight := ctx.FormInt("last_right")
	idxLeft := ctx.FormInt("left")
	idxRight := ctx.FormInt("right")
	leftHunkSize := ctx.FormInt("left_hunk_size")
	rightHunkSize := ctx.FormInt("right_hunk_size")
	anchor := ctx.FormString("anchor")
	direction := ctx.FormString("direction")
	filePath := ctx.FormString("path")
	gitRepo := ctx.Repo.GitRepo
	if ctx.FormBool("wiki") {
		var err error
		gitRepo, err = git.OpenRepository(ctx, ctx.Repo.Repository.WikiPath())
		if err != nil {
			ctx.ServerError("OpenRepository", err)
			return
		}
		defer gitRepo.Close()
	}
	chunkSize := gitdiff.BlobExcerptChunkSize
	commit, err := gitRepo.GetCommit(commitID)
	if err != nil {
		ctx.Error(http.StatusInternalServerError, "GetCommit")
		return
	}
	section := &gitdiff.DiffSection{
		FileName: filePath,
		Name:     filePath,
	}
	if direction == "up" && (idxLeft-lastLeft) > chunkSize {
		idxLeft -= chunkSize
		idxRight -= chunkSize
		leftHunkSize += chunkSize
		rightHunkSize += chunkSize
		section.Lines, err = getExcerptLines(commit, filePath, idxLeft-1, idxRight-1, chunkSize)
	} else if direction == "down" && (idxLeft-lastLeft) > chunkSize {
		section.Lines, err = getExcerptLines(commit, filePath, lastLeft, lastRight, chunkSize)
		lastLeft += chunkSize
		lastRight += chunkSize
	} else {
		offset := -1
		if direction == "down" {
			offset = 0
		}
		section.Lines, err = getExcerptLines(commit, filePath, lastLeft, lastRight, idxRight-lastRight+offset)
		leftHunkSize = 0
		rightHunkSize = 0
		idxLeft = lastLeft
		idxRight = lastRight
	}
	if err != nil {
		ctx.Error(http.StatusInternalServerError, "getExcerptLines")
		return
	}
	if idxRight > lastRight {
		lineText := " "
		if rightHunkSize > 0 || leftHunkSize > 0 {
			lineText = fmt.Sprintf("@@ -%d,%d +%d,%d @@\n", idxLeft, leftHunkSize, idxRight, rightHunkSize)
		}
		lineText = html.EscapeString(lineText)
		lineSection := &gitdiff.DiffLine{
			Type:    gitdiff.DiffLineSection,
			Content: lineText,
			SectionInfo: &gitdiff.DiffLineSectionInfo{
				Path:          filePath,
				LastLeftIdx:   lastLeft,
				LastRightIdx:  lastRight,
				LeftIdx:       idxLeft,
				RightIdx:      idxRight,
				LeftHunkSize:  leftHunkSize,
				RightHunkSize: rightHunkSize,
			},
		}
		if direction == "up" {
			section.Lines = append([]*gitdiff.DiffLine{lineSection}, section.Lines...)
		} else if direction == "down" {
			section.Lines = append(section.Lines, lineSection)
		}
	}
	ctx.Data["section"] = section
	ctx.Data["FileNameHash"] = base.EncodeSha1(filePath)
	ctx.Data["AfterCommitID"] = commitID
	ctx.Data["Anchor"] = anchor
	ctx.HTML(http.StatusOK, tplBlobExcerpt)
}

func getExcerptLines(commit *git.Commit, filePath string, idxLeft, idxRight, chunkSize int) ([]*gitdiff.DiffLine, error) {
	blob, err := commit.Tree.GetBlobByPath(filePath)
	if err != nil {
		return nil, err
	}
	reader, err := blob.DataAsync()
	if err != nil {
		return nil, err
	}
	defer reader.Close()
	scanner := bufio.NewScanner(reader)
	var diffLines []*gitdiff.DiffLine
	for line := 0; line < idxRight+chunkSize; line++ {
		if ok := scanner.Scan(); !ok {
			break
		}
		if line < idxRight {
			continue
		}
		lineText := scanner.Text()
		diffLine := &gitdiff.DiffLine{
			LeftIdx:  idxLeft + (line - idxRight) + 1,
			RightIdx: line + 1,
			Type:     gitdiff.DiffLinePlain,
			Content:  " " + lineText,
		}
		diffLines = append(diffLines, diffLine)
	}
	return diffLines, nil
}