%%% BEGIN openflax/handler/svn.erl %%% %%% %%% openflax - Open Source web server for Erlang/OTP %%% Copyright (c)2004 Cat's Eye Technologies. All rights reserved. %%% %%% Redistribution and use in source and binary forms, with or without %%% modification, are permitted provided that the following conditions %%% are met: %%% %%% Redistributions of source code must retain the above copyright %%% notice, this list of conditions and the following disclaimer. %%% %%% Redistributions in binary form must reproduce the above copyright %%% notice, this list of conditions and the following disclaimer in %%% the documentation and/or other materials provided with the %%% distribution. %%% %%% Neither the name of Cat's Eye Technologies nor the names of its %%% contributors may be used to endorse or promote products derived %%% from this software without specific prior written permission. %%% %%% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND %%% CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, %%% INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF %%% MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE %%% DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE %%% LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, %%% OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, %%% PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, %%% OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON %%% ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, %%% OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY %%% OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE %%% POSSIBILITY OF SUCH DAMAGE. %% @doc Subverlsion handler for OpenFlax. %% %%

Browse a Subversion repository.

%% %% %% %% @end -module(openflax.handler.svn). -vsn('$Id: svn.erl 35 2004-09-19 21:28:09Z catseye $'). -author('cpressey@catseye.mine.nu'). -copyright('Copyright (c)2004 Cat`s Eye Technologies. All rights reserved.'). -behaviour(openflax.handler). -export([start/1, stop/1, serve/1]). % behaviour -import(lists). -import(string). -import(filename). -import(file). -import(svn). %% @spec start(conf()) -> conf() %% @doc Initializes the Subverlsion handler. start(_Conf) -> openflax.conf:new(). %% @spec stop(conf()) -> conf() %% @doc Shuts down the Subverlsion handler. stop(Conf) -> Conf. %% @spec serve(conf()) -> ok | {error, Reason} %% @doc Serves any of a number of things from a Subversion %% repository. Currently this includes: the youngest revision %% of a file, file listings for directories in the repository, and %% the change log for each file (currently rather crude.) serve(Conf) -> URI = openflax.conf:get_string(sreq_basic_resource, Conf), Branch = case openflax.conf:get_string(arg_branch, Conf) of "" -> "current"; Else -> Else end, % Get the repo base - this is where the repository roots are stored. RepoSite = openflax.conf:get_string(cfg_svn_repo_site, Conf), RepoBase = openflax.conf:get_string(cfg_svn_repo_base, Conf), RepoRoot = openflax.conf:get_string(cfg_svn_repo_root, Conf), % Turn the URI into a repository name and a file name. % /repo/project/src/foo.c -> svn://site/base/project/branch/src/foo.c ["/", WebRepoIndex, RepoName | Rest] = filename:split(URI), Path = filename:join(["/"] ++ Rest), FilePath = filename:join(["/", Branch] ++ Rest), % Before anything else, check to see if arg_path was given, and if so, redirect. case openflax.conf:get_string(arg_path, Conf) of "" -> serve0(Conf, URI, Branch, RepoSite, RepoBase, RepoRoot, RepoName, Path, FilePath); NewPath -> NewPath0 = "/" ++ WebRepoIndex ++ "/" ++ RepoName ++ NewPath ++ "?" ++ reassemble_args(Conf, [branch, rev, log, base, sortby, keyword]), {{moved_permanently, NewPath0}, Conf} end. serve0(Conf, URI, Branch, RepoSite, RepoBase, RepoRoot, RepoName, Path, FilePath) -> Repo = svn:open("svn://" ++ RepoSite ++ RepoBase ++ RepoName), % Determine the desired revision. HeadRev = svn:head(Repo), {IsHead, Rev} = case openflax.conf:get_string(arg_rev, Conf) of "" -> {true, HeadRev}; RevStr -> case catch list_to_integer(RevStr) of RevInt when is_integer(RevInt), RevInt > 0, RevInt < HeadRev -> {false, RevInt}; _ -> {true, HeadRev} end end, Conf0 = openflax.conf:put_value(tpl_svn_repository, RepoName, Conf), Conf1 = openflax.conf:put_value(tpl_svn_path, Path, Conf0), Conf2 = openflax.conf:put_value(tpl_svn_branch, Branch, Conf1), Conf3 = openflax.conf:put_value(tpl_svn_rev, openflax.string:from_term(Rev), Conf2), NextRevLink = case HeadRev == Rev of true -> ""; false -> ">>" end, PrevRevLink = case Rev == 1 of true -> ""; false -> "<<" end, Conf4 = openflax.conf:put_value(tpl_svn_prev_rev_link, PrevRevLink, Conf3), Conf5 = openflax.conf:put_value(tpl_svn_next_rev_link, NextRevLink, Conf4), Branches = svn:list(Repo, "/", Rev), BranchOptions = lists:flatten(lists:map( fun({BranchName, _BranchType, _BranchRev, _BranchBlame, _BranchSize, _BranchDate}) -> case BranchName of Branch -> [""]; _ -> [""] end end, Branches)), BranchReleases = lists:flatten(lists:map( fun({BranchName, _BranchType, _BranchRev, _BranchBlame, _BranchSize, _BranchDate}) -> case BranchName of "current" -> []; _ -> ArchiveBaseName = [RepoName, "-", BranchName], ["
  • ", lists:map(fun(Extension) -> ["", ArchiveBaseName, Extension, " "] end, [".tgz", ".zip"]), "
  • "] end end, Branches)), Conf6 = openflax.conf:put_value(tpl_svn_branch_options, BranchOptions, Conf5), Conf7 = openflax.conf:put_value(tpl_svn_branch_releases, BranchReleases, Conf6), Conf8 = openflax.conf:put_value(tpl_svn_path_options, assemble_path_options(Path), Conf7), Result = (catch case openflax.conf:get_string(arg_log, Conf8) of "" -> case lists:last(URI) of $/ -> case openflax.conf:get_string(arg_view, Conf8) of "tree" -> serve_tree(Repo, Branch, FilePath, Rev, IsHead, Conf8); _ -> serve_list(Repo, Branch, FilePath, Rev, IsHead, Conf8) end; _ -> case openflax.conf:get_string(arg_base, Conf8) of "" -> serve_file(Repo, Branch, FilePath, Rev, Conf8); BaseRevStr -> BaseRev = case catch list_to_integer(BaseRevStr) of BaseRevInt when is_integer(BaseRevInt), BaseRevInt > 0, BaseRevInt =< HeadRev -> BaseRevInt; _ -> Rev - 1 end, FileRepo = svn:open("file://" ++ RepoRoot ++ RepoName), DiffResult = serve_diff(FileRepo, Branch, FilePath, BaseRev, Rev, Conf8), svn:close(FileRepo), DiffResult end end; LogSpec -> serve_log(Repo, Branch, FilePath, LogSpec, URI, Conf8) end), svn:close(Repo), case Result of {ok, RealResult} -> RealResult; svn_invalid_filetype -> % try to redirect to a directory if it looks like % the trailing slash has been omitted. case lists:last(URI) of $/ -> {not_found, Conf}; _ -> {{moved_permanently, URI ++ "/"}, Conf} end; ElseResult -> openflax.app:debug(svn_error_result, ElseResult), {not_found, Conf8} end. reassemble_args(Conf, ArgList) -> lists:flatten(reassemble_args0(Conf, ArgList, [])). reassemble_args0(Conf, [Arg | Tail], Acc) -> ArgName = atom_to_list(Arg), ArgKey = list_to_atom("arg_" ++ ArgName), case {openflax.conf:get_string(ArgKey, Conf), Acc} of {"", _} -> reassemble_args0(Conf, Tail, Acc); {ArgVal, []} -> [ArgName, "=", ArgVal]; {ArgVal, _} -> [Acc, "&", ArgName, "=", ArgVal] end. assemble_path_options(Path) -> lists:reverse(assemble_path_options0(filename:split(Path), Path, [], [])). assemble_path_options0([], _ThisPath, _AccPath, AccOpts) -> AccOpts; assemble_path_options0([PathComponent | Tail], ThisPath, AccPath, AccOpts) -> AccPath0 = case AccPath of "" -> PathComponent; "/" -> AccPath ++ PathComponent; _ -> AccPath ++ "/" ++ PathComponent end, PathName = case AccPath0 of ThisPath -> ""; _ -> "" end, AccOpts0 = [PathName | AccOpts], assemble_path_options0(Tail, ThisPath, AccPath0, AccOpts0). sort_files_by("size") -> fun ({_, _, _, _, A, _}, {_, _, _, _, B, _}) -> A > B end; sort_files_by("date") -> fun ({_, _, _, _, _, A}, {_, _, _, _, _, B}) -> A > B end; sort_files_by(_) -> fun ({A, _, _, _, _, _}, {B, _, _, _, _, _}) -> A > B end. serve_list(Repo, _Branch, DirName, Rev, IsHead, Conf) -> Files = lists:sort(sort_files_by(openflax.conf:get_string(arg_sortby, Conf)), svn:list(Repo, DirName, Rev)), EachFile = openflax.conf:get_string(cfg_svn_template_each_file, Conf), EachDir = openflax.conf:get_string(cfg_svn_template_each_subdir, Conf), Keyword = openflax.conf:get_string(arg_keyword, Conf), {FBody, DBody} = lists:foldl(fun ({Filename, FileType, FileRev, FileBlame, FileSize, FileDate}, {FAcc, DAcc}) -> FullFilename = filename:join([DirName, Filename]), FConf0 = openflax.conf:put_value(tpl_svn_filename, Filename, Conf), FileLink = case IsHead of true -> urlize(Filename); false -> urlize(Filename) ++ "?rev=" ++ integer_to_list(Rev) end, FConf1 = openflax.conf:put_value(tpl_svn_filelink, FileLink, FConf0), FConf2 = openflax.conf:put_value(tpl_svn_filesize, openflax.string:from_term(FileSize), FConf1), FConf3 = openflax.conf:put_value(tpl_svn_filedate, FileDate, FConf2), FConf4 = openflax.conf:put_value(tpl_svn_fileblame, FileBlame, FConf3), FConf5 = openflax.conf:put_value(tpl_svn_filerev, openflax.string:from_term(FileRev), FConf4), FConf6 = openflax.conf:put_value(tpl_svn_filedesc, svn:propget_default(Repo, FullFilename, "openflax:description", Rev, ""), FConf5), Keywords = string:tokens(svn:propget_default(Repo, FullFilename, "openflax:keywords", Rev, ""), " "), KeywordsString = [ ["", KW, " "] || KW <- Keywords ], FConf7 = openflax.conf:put_value(tpl_svn_filekeywords, KeywordsString, FConf6), FConf = FConf7, case {matches(Keyword, Keywords), FileType} of {false, _} -> {FAcc, DAcc}; {true, dir} -> DAcc0 = [openflax.handler.template:fill_out(EachDir, FConf) | DAcc], {FAcc, DAcc0}; {true, _} -> FAcc0 = [openflax.handler.template:fill_out(EachFile, FConf) | FAcc], {FAcc0, DAcc} end end, {[], []}, Files), Conf0 = openflax.conf:put_value(tpl_svn_filebody, FBody, Conf), Conf1 = openflax.conf:put_value(tpl_svn_dirbody, DBody, Conf0), Conf2 = openflax.conf:put_value(tpl_svn_motd, svn:propget_default(Repo, DirName, "openflax:motd", Rev, ""), Conf1), LinksText = svn:propget_default(Repo, DirName, "openflax:links", Rev, ""), LinksHTML = case catch parse_links(LinksText, Conf) of L when is_list(L) -> L; _ -> "" end, Conf3 = openflax.conf:put_value(tpl_svn_links, LinksHTML, Conf2), Conf4 = openflax.conf:put_value(tpl_svn_headings, openflax.conf:get_string(cfg_svn_list_headings, Conf3), Conf3), Conf5 = openflax.conf:put_value(res_content_type, "text/html", Conf4), {ok, {openflax.handler.template, Conf5}}. serve_tree(Repo, Branch, DirName, Rev, _IsHead, Conf) -> Tree = svn:tree(Repo, DirName, Rev), TBody = "", Conf0 = openflax.conf:put_value(tpl_svn_filebody, TBody, Conf), %Conf1 = openflax.conf:put_value(tpl_svn_dirbody, DBody, Conf0), %Conf2 = openflax.conf:put_value(tpl_svn_motd, % svn:propget(Repo, DirName, "openflax:motd", Rev), Conf1), %LinksText = svn:propget(Repo, DirName, "openflax:links", Rev), %LinksHTML = case catch parse_links(LinksText, Conf) of % L when is_list(L) -> L; % _ -> "" %end, %Conf3 = openflax.conf:put_value(tpl_svn_links, LinksHTML, Conf2), Conf4 = openflax.conf:put_value(res_content_type, "text/html", Conf0), {ok, {openflax.handler.template, Conf4}}. fold_tree([], _Branch, _DirName, Acc) -> lists:reverse(Acc); fold_tree([{file, Entry} | Rest], Branch, DirName, Acc) -> {Filename, FileType, FileRev, FileBlame, FileSize, FileDate} = Entry, FullFilename = filename:join([DirName, Filename]), FullFilename0 = string:substr(FullFilename, length(Branch) + 3), Text = "
  • " ++ Filename ++ "
  • \n", fold_tree(Rest, Branch, DirName, [Text | Acc]); fold_tree([{dir, Entry, SubTree} | Rest], Branch, DirName, Acc) -> {Filename, FileType, FileRev, FileBlame, FileSize, FileDate} = Entry, FullFilename = filename:join([DirName, Filename]), FullFilename0 = string:substr(FullFilename, length(Branch) + 3), Text = "
  • " ++ Filename ++ "
  • \n", fold_tree(Rest, Branch, DirName, [Text | Acc]). serve_file(Repo, _Branch, Filename, Rev, Conf) -> Content = svn:cat(Repo, Filename, Rev), % if available, use the content type that svn provides (sweet!) ContentType = svn:propget_default(Repo, Filename, "svn:mime-type", Rev, "text/plain"), Conf0 = openflax.conf:put_value(res_content_type, ContentType, Conf), {ok, {{content, Content}, Conf0}}. serve_log(Repo, _Branch, Filename, _LogSpec, _URI, Conf) -> EachLogEntry = openflax.conf:get_string(cfg_svn_template_each_log_entry, Conf), LogEntries = svn:log(Repo, Filename), Content = lists:foldl(fun({Rev, Blame, Date, Message}, Acc) -> RevStr = integer_to_list(Rev), BaseRevStr = integer_to_list(Rev - 1), FConf0 = openflax.conf:put_value(tpl_svn_rev, RevStr, Conf), FConf1 = openflax.conf:put_value(tpl_svn_prevrev, BaseRevStr, FConf0), FConf2 = openflax.conf:put_value(tpl_svn_blame, Blame, FConf1), FConf3 = openflax.conf:put_value(tpl_svn_date, Date, FConf2), FConf4 = openflax.conf:put_value(tpl_svn_message, Message, FConf3), [openflax.handler.template:fill_out(EachLogEntry, FConf4) | Acc] end, [], LogEntries), Conf0 = openflax.conf:put_value(tpl_svn_filebody, Content, Conf), Conf3 = openflax.conf:put_value(tpl_svn_headings, openflax.conf:get_string(cfg_svn_log_headings, Conf0), Conf0), Conf4 = openflax.conf:put_value(res_content_type, "text/html", Conf3), {ok, {openflax.handler.template, Conf4}}. serve_diff(Repo, _Branch, Filename, BaseRev, Rev, Conf) -> Diff = svn:diff(Repo, Filename, BaseRev, Rev), Content = parse_diff(string:tokens(Diff, "\n"), [], Conf), Conf0 = openflax.conf:put_value(tpl_svn_filebody, Content, Conf), Conf3 = openflax.conf:put_value(tpl_svn_headings, openflax.conf:get_string(cfg_svn_diff_headings, Conf0), Conf0), Conf4 = openflax.conf:put_value(res_content_type, "text/html", Conf3), {ok, {openflax.handler.template, Conf4}}. parse_diff([], Acc, Conf) -> lists:reverse(Acc); parse_diff(["Property" ++ _ | _Tail], Acc, Conf) -> parse_diff([], Acc, Conf); parse_diff(["Index: " ++ _Filename | Tail], Acc, Conf) -> parse_diff(Tail, Acc, Conf); parse_diff(["=" ++ _EqualsLine | Tail], Acc, Conf) -> parse_diff(Tail, Acc, Conf); parse_diff(["--- " ++ Filename1, "+++ " ++ Filename2 | Tail], Acc, Conf) -> parse_diff(Tail, [fmt_diff_row( cfg_svn_template_each_diff_filename, Filename1, cfg_svn_template_each_diff_filename, Filename2, Conf) | Acc], Conf); parse_diff([" " ++ PlainLine | Tail], Acc, Conf) -> parse_diff(Tail, [fmt_diff_row( cfg_svn_template_each_diff_unchanged, PlainLine, cfg_svn_template_each_diff_unchanged, PlainLine, Conf) | Acc], Conf); parse_diff([OffsetLine = "@@" ++ _ | Tail], Acc, Conf) -> parse_diff(Tail, [fmt_diff_row( cfg_svn_template_each_diff_offset, OffsetLine, cfg_svn_template_each_diff_offset, " ", Conf) | Acc], Conf); parse_diff(["+" ++ AddLine | Tail], Acc, Conf) -> parse_diff(Tail, [fmt_diff_row( cfg_svn_template_each_diff_removeline, " ", cfg_svn_template_each_diff_addline, AddLine, Conf) | Acc], Conf); parse_diff(["-" ++ SubLine | Tail], Acc, Conf) -> parse_diff(Tail, [fmt_diff_row( cfg_svn_template_each_diff_removeline, SubLine, cfg_svn_template_each_diff_addline, " ", Conf) | Acc], Conf). fmt_diff_row(LeftType, LeftString, RightType, RightString, Conf) -> [ "", fmt_diff_cell(LeftType, LeftString, Conf), fmt_diff_cell(RightType, RightString, Conf), "" ]. fmt_diff_cell(Type, String, Conf) -> Fmt = openflax.conf:get_string(Type, Conf), Conf0 = openflax.conf:put_value(tpl_svn_arg, htmlize(String), Conf), openflax.handler.template:fill_out(Fmt, Conf0). %%% UTILITY %%% parse_links(LinksText, Conf) -> EachLink = openflax.conf:get_string(cfg_svn_template_each_link, Conf), lists:map(fun(LinkLine) -> [URL, LinkName, LinkDesc | LinkTail] = string:tokens(LinkLine, "|"), LinkKeywords = case LinkTail of [LKW | _] -> string:tokens(LKW, " "); _ -> [] end, LConf0 = openflax.conf:put_value(tpl_svn_linkurl, URL, Conf), LConf1 = openflax.conf:put_value(tpl_svn_linkdesc, LinkDesc, LConf0), LConf2 = openflax.conf:put_value(tpl_svn_linkname, LinkName, LConf1), LConf3 = openflax.conf:put_value(tpl_svn_linkkeywords, LinkKeywords, LConf2), LConf = LConf3, openflax.handler.template:fill_out(EachLink, LConf) end, string:tokens(LinksText, "\n")). matches("", _) -> true; matches(Keyword, Keywords) -> lists:member(Keyword, Keywords). urlize(Filename) -> Filename0 = lists:map(fun ($ ) -> "%20"; ($?) -> "%3f"; ($#) -> "%23"; (Else) -> Else end, Filename), lists:flatten(Filename0). htmlize(String) -> String0 = lists:map(fun ($<) -> "<"; ($>) -> ">"; (Else) -> Else end, String), lists:flatten(String0). %%% END of openflax/handler/svn.erl %%%