diff --git a/docs/uri-scheme.rst b/docs/uri-scheme.rst index 9ff1d7a0..9826e1ab 100644 --- a/docs/uri-scheme.rst +++ b/docs/uri-scheme.rst @@ -1,487 +1,530 @@ URI scheme for SWH Web applications =================================== SWH Browse Urls --------------- This web application aims to provide HTML views to easily navigate in the SWH archive, thus it needs to be reached from a web browser. If you intend to query the SWH archive programmatically through any HTTP client, please refer to the `SWH Web API URLs`_ instead. Content ^^^^^^^ .. http:get:: /browse/content/[(algo_hash):](hash)/ HTML view that displays a SWH content identified by its hash value. If the content to display is textual, it will be highlighted client-side if possible using highlightjs_. In order for that operation to be performed, a programming language must first be associated to the content. The following procedure is used in order to find the language: 1) First try to find a language from the content filename (provided as query parameter when navigating from a directory view). 2) If no language has been found from the filename, try to find one from the content mime type. The mime type is retrieved from the content metadata stored in the SWH archive or is computed server-side using Python magic module. When that view is called in the context of a navigation coming from a directory view, a breadcrumb will be displayed on top of the rendered content in order to easily navigate up to the associated root directory. In that case, the path query parameter will be used and filled with the path of the file relative to the root directory. :param algo_hash: optionnal parameter to indicate the algorithm used to compute the content checksum (default to *sha1*) :type algo_hash: a string identifying the hashing algorithm (either *sha1*, *sha1_git*, *sha256* or *blake2s256*) :param hash: the checksum from which to retrieve the associated content in the SWH archive :type hash: hexadecimal representation of the hash value :query path: optionnal parameter used to describe the path of the content relative to a root directory (used to add context aware navigation links when navigating from a directory view) :type path: string :statuscode 200: no error :statuscode 400: an invalid query string has been provided :statuscode 404: requested content can not be found in the SWH archive .. http:get:: /browse/content/[(algo_hash):](hash)/raw/ HTML view that produces a raw display of a SWH content identified by its hash value. The behaviour of that view depends on the mime type of the requested content. If the mime type is from the text family, the view will return a response whose content type is 'text/plain' that will be rendered by the browser. Otherwise, the view will return a response whose content type is 'application/octet-stream' and the browser will then offer to download the file. In the context of a navigation coming from a directory view, the filename query parameter will be used in order to provide the real name of the file when one wants to save it locally. :param algo_hash: optionnal parameter to indicate the algorithm used to compute the content checksum (default to *sha1*) :type algo_hash: a string identifying the hashing algorithm (either *sha1*, *sha1_git*, *sha256* or *blake2s256*) :param hash: the checksum from which to retrieve the associated content in the SWH archive :type hash: hexadecimal representation of the hash value :query filename: optionnal parameter used to indicate the name of the file holding the requested content (used when one wants to save the content to a local file) :type path: string :statuscode 200: no error :statuscode 400: an invalid query string has been provided :statuscode 404: requested content can not be found in the SWH archive Directory ^^^^^^^^^ .. http:get:: /browse/directory/(sha1_git)/ HTML view for browsing the content of a SWH directory identified by its `sha1_git` value. The content of the directory is first sorted in lexicographical order and the sub-directories are displayed before the regular files. The view enables to navigate from the provided root directory to directories reachable from it in a recursive way. A breadcrumb located in the top part of the view allows to keep track of the paths navigated so far. :param sha1_git: the `sha1_git` identifier of the directory to browse :type sha1_git: hexadecimal representation of that hash value :statuscode 200: no error :statuscode 400: an invalid `sha1_git` value has been provided :statuscode 404: requested directory can not be found in the SWH archive .. http:get:: /browse/directory/(sha1_git)/(path)/ HTML view for browsing the content of a SWH directory reachable from the provided root one identified by its `sha1_git` value. The content of the directory is first sorted in lexicographical order and the sub-directories are displayed before the regular files. The view enables to navigate from the requested directory to directories reachable from it in a recursive way but also up to the root directory. A breadcrumb located in the top part of the view allows to keep track of the paths navigated so far. :param sha1_git: the `sha1_git` identifier of the directory to browse :type sha1_git: hexadecimal representation of that hash value :param path: path of a directory reachable from the provided root one :type path: string :statuscode 200: no error :statuscode 400: an invalid `sha1_git` value has been provided :statuscode 404: requested directory can not be found in the SWH archive Origin ^^^^^^ Origin metadata """"""""""""""" .. http:get:: /browse/origin/(origin_id)/ HTML view that displays a SWH origin identified by its id. The view displays the origin metadata and contains links for browsing its directories and contents for each SWH visit. :param origin_id: the id of a SWH origin :type origin_id: int :statuscode 200: no error :statuscode 404: requested origin can not be found in the SWH archive .. http:get:: /browse/origin/(origin_type)/url/(origin_url)/ HTML view that displays a SWH origin identified by its type and url. The view displays the origin metadata and contains links for browsing its directories and contents for each SWH visit. :param origin_type: the type of the SWH origin (*git*, *svn*, ...) :type origin_type: string :param origin_url: the url of the origin (e.g. https://github.com///) :type origin_url: string :statuscode 200: no error :statuscode 404: requested origin can not be found in the SWH archive Origin directory """""""""""""""" .. http:get:: /browse/origin/(origin_id)/directory/ HTML view for browsing the content of the root directory associated to the latest visit of a SWH origin. The content of the directory is first sorted in lexicographical order and the sub-directories are displayed before the regular files. The view enables to navigate from the origin root directory to directories reachable from it in a recursive way. A breadcrumb located in the top part of the view allows to keep track of the paths navigated so far. The view also enables to easily switch between the origin branches through a dropdown menu. The origin branch (default to master) from which to retrieve the directory content can also be specified by using the branch query parameter. :param origin_id: the id of a SWH origin :type origin_id: int :query branch: optional query parameter to specify the origin branch from which to retrieve the directory :type branch: string :statuscode 200: no error :statuscode 404: requested origin can not be found in the SWH archive .. http:get:: /browse/origin/(origin_id)/directory/(path)/ HTML view for browsing the content of a directory reachable from the root directory associated to the latest visit of a SWH origin. The content of the directory is first sorted in lexicographical order and the sub-directories are displayed before the regular files. The view enables to navigate from the requested directory to directories reachable from it in a recursive way but also up to the origin root directory. A breadcrumb located in the top part of the view allows to keep track of the paths navigated so far. The view also enables to easily switch between the origin branches through a dropdown menu. The origin branch (default to master) from which to retrieve the directory content can also be specified by using the branch query parameter. :param origin_id: the id of a SWH origin :type origin_id: int :param path: path of a directory reachable from the origin root one :type path: string :query branch: optional query parameter to specify the origin branch from which to retrieve the directory :type branch: string :statuscode 200: no error :statuscode 404: requested origin can not be found in the SWH archive or the provided path does not exist from the origin root directory .. http:get:: /browse/origin/(origin_id)/visit/(visit_id)/directory/ HTML view for browsing the content of the root directory associated to a specific visit (identified by its id) of a SWH origin. The content of the directory is first sorted in lexicographical order and the sub-directories are displayed before the regular files. The view enables to navigate from the origin root directory to directories reachable from it in a recursive way. A breadcrumb located in the top part of the view allows to keep track of the paths navigated so far. The view also enables to easily switch between the origin branches through a dropdown menu. The origin branch (default to master) from which to retrieve the directory content can also be specified by using the branch query parameter. :param origin_id: the id of a SWH origin :type origin_id: int :param visit_id: the id of the origin visit :type visit_id: int :query branch: optional query parameter to specify the origin branch from which to retrieve the directory :type branch: string :statuscode 200: no error :statuscode 404: requested origin can not be found in the SWH archive or requested visit id does not exist .. http:get:: /browse/origin/(origin_id)/visit/(visit_id)/directory/(path)/ HTML view for browsing the content of a directory reachable from the root directory associated to a specific visit (identified by its id) of a SWH origin. The content of the directory is first sorted in lexicographical order and the sub-directories are displayed before the regular files. The view enables to navigate from the requested directory to directories reachable from it in a recursive way but also up to the origin root directory. A breadcrumb located in the top part of the view allows to keep track of the paths navigated so far. The view also enables to easily switch between the origin branches through a dropdown menu. The origin branch (default to master) from which to retrieve the directory content can also be specified by using the branch query parameter. :param origin_id: the id of a SWH origin :type origin_id: int :param visit_id: the id of the origin visit :type visit_id: int :param path: path of a directory reachable from the origin root one :type path: string :query branch: optional query parameter to specify the origin branch from which to retrieve the directory :type branch: string :statuscode 200: no error :statuscode 404: requested origin can not be found in the SWH archive, requested visit id does not exist or the provided path does not exist from the origin root directory .. http:get:: /browse/origin/(origin_id)/ts/(ts)/directory/ HTML view for browsing the content of the root directory associated to a specific visit (identified by its timestamp) of a SWH origin. The content of the directory is first sorted in lexicographical order and the sub-directories are displayed before the regular files. The view enables to navigate from the origin root directory to directories reachable from it in a recursive way. A breadcrumb located in the top part of the view allows to keep track of the paths navigated so far. The view also enables to easily switch between the origin branches through a dropdown menu. The origin branch (default to master) from which to retrieve the directory content can also be specified by using the branch query parameter. :param origin_id: the id of a SWH origin :type origin_id: int :param ts: the timestamp of the origin visit :type ts: Unix timestamp :query branch: optional query parameter to specify the origin branch from which to retrieve the directory :type branch: string :statuscode 200: no error :statuscode 404: requested origin can not be found in the SWH archive or requested visit timestamp does not exist .. http:get:: /browse/origin/(origin_id)/ts/(ts)/directory/(path)/ HTML view for browsing the content of a directory reachable from the root directory associated to a specific visit (identified by its timestamp) of a SWH origin. The content of the directory is first sorted in lexicographical order and the sub-directories are displayed before the regular files. The view enables to navigate from the requested directory to directories reachable from it in a recursive way but also up to the origin root directory. A breadcrumb located in the top part of the view allows to keep track of the paths navigated so far. The view also enables to easily switch between the origin branches through a dropdown menu. The origin branch (default to master) from which to retrieve the directory content can also be specified by using the branch query parameter. :param origin_id: the id of a SWH origin :type origin_id: int :param ts: the timestamp of the origin visit :type ts: Unix timestamp :param path: path of a directory reachable from the origin root one :type path: string :query branch: optional query parameter to specify the origin branch from which to retrieve the directory :type branch: string :statuscode 200: no error :statuscode 404: requested origin can not be found in the SWH archive, requested visit timestamp does not exist or the provided path does not exist from the origin root directory Origin content """""""""""""" .. http:get:: /browse/origin/(origin_id)/content/(path)/ HTML view that produces an HTML display of a SWH content associated to the latest visit of a SWH origin. If the content to display is textual, it will be highlighted client-side if possible using highlightjs_. In order for that operation to be performed, a programming language must first be associated to the content. The following procedure is used in order to find the language: 1) First try to find a language from the content filename 2) If no language has been found from the filename, try to find one from the content mime type. The mime type is retrieved from the content metadata stored in the SWH archive or is computed server-side using Python magic module. The view displays a breadcrumb on top of the rendered content in order to easily navigate up to the origin root directory. The view also enables to easily switch between the origin branches through a dropdown menu. The origin branch (default to master) from which to retrieve the content can also be specified by using the branch query parameter. :param origin_id: the id of a SWH origin :type origin_id: int :param path: path of a content reachable from the origin root directory :type path: string :query branch: optional query parameter to specify the origin branch from which to retrieve the content :type branch: string :statuscode 200: no error :statuscode 404: requested origin can not be found in the SWH archive, or the provided content path does not exist from the origin root directory .. http:get:: /browse/origin/(origin_id)/visit/(visit_id)/content/(path)/ HTML view that produces an HTML display of a SWH content associated to a specific visit (identified by its id) of a SWH origin. If the content to display is textual, it will be highlighted client-side if possible using highlightjs_. In order for that operation to be performed, a programming language must first be associated to the content. The following procedure is used in order to find the language: 1) First try to find a language from the content filename 2) If no language has been found from the filename, try to find one from the content mime type. The mime type is retrieved from the content metadata stored in the SWH archive or is computed server-side using Python magic module. The view displays a breadcrumb on top of the rendered content in order to easily navigate up to the origin root directory. The view also enables to easily switch between the origin branches through a dropdown menu. The origin branch (default to master) from which to retrieve the content can also be specified by using the branch query parameter. :param origin_id: the id of a SWH origin :type origin_id: int :param visit_id: the id of the origin visit :type visit_id: int :param path: path of a content reachable from the origin root directory :type path: string :query branch: optional query parameter to specify the origin branch from which to retrieve the content :type branch: string :statuscode 200: no error :statuscode 404: requested origin can not be found in the SWH archive, requested visit id does not exist or the provided content path does not exist from the origin root directory .. http:get:: /browse/origin/(origin_id)/ts/(ts)/content/(path)/ HTML view that produces an HTML display of a SWH content associated to a specific visit (identified by its timestamp) of a SWH origin. If the content to display is textual, it will be highlighted client-side if possible using highlightjs_. In order for that operation to be performed, a programming language must first be associated to the content. The following procedure is used in order to find the language: 1) First try to find a language from the content filename 2) If no language has been found from the filename, try to find one from the content mime type. The mime type is retrieved from the content metadata stored in the SWH archive or is computed server-side using Python magic module. The view displays a breadcrumb on top of the rendered content in order to easily navigate up to the origin root directory. The view also enables to easily switch between the origin branches through a dropdown menu. The origin branch (default to master) from which to retrieve the content can also be specified by using the branch query parameter. :param origin_id: the id of a SWH origin :type origin_id: int :param ts: the timestamp of the origin visit :type ts: Unix timestamp :param path: path of a content reachable from the origin root directory :type path: string :query branch: optional query parameter to specify the origin branch from which to retrieve the content :type branch: string :statuscode 200: no error :statuscode 404: requested origin can not be found in the SWH archive, requested visit timestamp does not exist or the provided content path does not exist from the origin root directory Person ^^^^^^ .. http:get:: /browse/person/(person_id)/ HTML view that displays information regarding a SWH person. :param person_id: the id of a SWH person :type origin_id: int :statuscode 200: no error :statuscode 404: requested person can not be found in the SWH archive +Revision +^^^^^^^^ + +.. http:get:: /browse/revision/(revision_id)/ + + HTML view that displays the metadata associated to a SWH revision. + It notably shows the revision date and message but also offers + links to get more details on: + + * the author + * the committer + * the directory that revision points to + * the history log reachable from that revision + + :param revision_id: the sha1_git identifier of a SWH revision + :type revision_id: hexadecimal representation of that hash value + :statuscode 200: no error + :statuscode 404: requested revision can not be found in the SWH archive + +.. http:get:: /browse/revision/(revision_id)/log/ + + HTML view that displays the list of revisions heading to + a given one. In other words, it shows a commit log. + The following data are displayed for each log entry: + + * author of the revision + * link to the revision metadata + * message for the revision + * date of the revision + * link to browse the associated source tree + + :param revision_id: the sha1_git identifier of a SWH revision + :type revision_id: hexadecimal representation of that hash value + :query revs_breadcrumb: query parameter used internally to store + the navigation breadcrumbs (i.e. the list of descendant revisions + visited so far). It must be a string in the form + "[//.../]" + :query per_page: the number of log entries to display per page + (default is 20, max is 50) + :statuscode 200: no error + :statuscode 404: requested revision can not be found in the SWH archive + + SWH Web API URLs ---------------- .. _highlightjs: https://highlightjs.org/ \ No newline at end of file diff --git a/swh/web/browse/urls.py b/swh/web/browse/urls.py index f6f580f6..3379ac71 100644 --- a/swh/web/browse/urls.py +++ b/swh/web/browse/urls.py @@ -1,41 +1,42 @@ # Copyright (C) 2017 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information from django.conf.urls import url from django.shortcuts import redirect from swh.web.common.utils import reverse import swh.web.browse.views.directory # noqa import swh.web.browse.views.content # noqa import swh.web.browse.views.origin # noqa import swh.web.browse.views.person # noqa +import swh.web.browse.views.revision # noqa from swh.web.browse.browseurls import BrowseUrls def default_browse_view(request): """Default django view used as an entry point for the swh browse ui web application. The url that point to it is /browse/. Currently, it points to the origin view for the linux kernel source tree github mirror. Args: request: input django http request """ linux_origin_id = '2' linux_origin_url = reverse('browse-origin', kwargs={'origin_id': linux_origin_id}) return redirect(linux_origin_url) urlpatterns = [ url(r'^$', default_browse_view) ] urlpatterns += BrowseUrls.get_url_patterns() diff --git a/swh/web/browse/views/origin.py b/swh/web/browse/views/origin.py index cfe51f42..15a69465 100644 --- a/swh/web/browse/views/origin.py +++ b/swh/web/browse/views/origin.py @@ -1,328 +1,328 @@ # Copyright (C) 2017 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information import dateutil from django.shortcuts import render from swh.web.common import service -from swh.web.common.utils import reverse +from swh.web.common.utils import reverse, format_utc_iso_date from swh.web.common.exc import NotFoundExc, handle_view_exception from swh.web.browse.utils import ( get_origin_visits, get_origin_visit_branches, gen_path_info, get_directory_entries, request_content, prepare_content_for_display ) from swh.web.browse.browseurls import browse_route @browse_route(r'origin/(?P[0-9]+)/', r'origin/(?P[a-z]+)/url/(?P.+)/', view_name='browse-origin') def origin_browse(request, origin_id=None, origin_type=None, origin_url=None): """Django view that produces an HTML display of a swh origin identified by its id or its url. The url scheme that points to it is the following: * :http:get:`/browse/origin/(origin_id)/` * :http:get:`/browse/origin/(origin_id)/directory/` Args: request: input django http request origin_id: a swh origin id origin_type: type of origin (git, svn, ...) origin_url: url of the origin (e.g. https://github.com//) Returns: The HMTL rendering for the metadata of the provided origin. """ try: if origin_id: origin_request_params = { 'id': origin_id, } else: origin_request_params = { 'type': origin_type, 'url': origin_url } origin_info = service.lookup_origin(origin_request_params) origin_id = origin_info['id'] origin_visits = get_origin_visits(origin_id) except Exception as exc: return handle_view_exception(exc) origin_info['last swh visit browse url'] = \ reverse('browse-origin-directory', kwargs={'origin_id': origin_id}) origin_visits_data = [] for visit in origin_visits: visit_date = dateutil.parser.parse(visit['date']) - visit['date'] = visit_date.strftime('%d %B %Y, %H:%M UTC') + visit['date'] = format_utc_iso_date(visit['date']) visit['browse_url'] = reverse('browse-origin-directory', kwargs={'origin_id': origin_id, 'visit_id': visit['visit']}) origin_visits_data.append( {'date': visit_date.timestamp()}) return render(request, 'origin.html', {'origin': origin_info, 'origin_visits_data': origin_visits_data, 'visits': origin_visits, 'browse_url_base': '/browse/origin/%s/' % origin_id}) @browse_route(r'origin/(?P[0-9]+)/directory/', r'origin/(?P[0-9]+)/directory/(?P.+)/', r'origin/(?P[0-9]+)/visit/(?P[0-9]+)/directory/', # noqa r'origin/(?P[0-9]+)/visit/(?P[0-9]+)/directory/(?P.+)/', # noqa r'origin/(?P[0-9]+)/ts/(?P[0-9]+)/directory/', r'origin/(?P[0-9]+)/ts/(?P[0-9]+)/directory/(?P.+)/', # noqa view_name='browse-origin-directory') def origin_directory_browse(request, origin_id, visit_id=None, ts=None, path=None): """Django view for browsing the content of a swh directory associated to an origin for a given visit. The url scheme that points to it is the following: * :http:get:`/browse/origin/(origin_id)/directory/` * :http:get:`/browse/origin/(origin_id)/directory/(path)/` * :http:get:`/browse/origin/(origin_id)/visit/(visit_id)/directory/` * :http:get:`/browse/origin/(origin_id)/visit/(visit_id)/directory/(path)/` * :http:get:`/browse/origin/(origin_id)/ts/(ts)/directory/` * :http:get:`/browse/origin/(origin_id)/ts/(ts)/directory/(path)/` Args: request: input django http request origin_id: a swh origin id visit_id: optionnal visit id parameter (the last one will be used by default) ts: optionnal visit timestamp parameter (the last one will be used by default) path: optionnal path parameter used to navigate in directories reachable from the origin root one Returns: The HTML rendering for the content of the directory associated to the provided origin and visit. """ # noqa try: if not visit_id and not ts: origin_visits = get_origin_visits(origin_id) return origin_directory_browse(request, origin_id, origin_visits[-1]['visit'], path=path) if not visit_id and ts: branches = get_origin_visit_branches(origin_id, visit_ts=ts) url_args = {'origin_id': origin_id, 'ts': ts} else: branches = get_origin_visit_branches(origin_id, visit_id) url_args = {'origin_id': origin_id, 'visit_id': visit_id} branch = request.GET.get('branch', 'master') filtered_branches = [b for b in branches if branch in b['name']] if len(filtered_branches) > 0: root_sha1_git = filtered_branches[0]['directory'] branch = filtered_branches[0]['name'] else: if visit_id: raise NotFoundExc('Branch %s associated to visit with' ' id %s for origin with id %s' ' not found!' % (branch, visit_id, origin_id)) else: raise NotFoundExc('Branch %s associated to visit with' ' timestamp %s for origin with id %s' ' not found!' % (branch, ts, origin_id)) sha1_git = root_sha1_git if path: dir_info = service.lookup_directory_with_path(root_sha1_git, path) sha1_git = dir_info['target'] dirs, files = get_directory_entries(sha1_git) except Exception as exc: return handle_view_exception(exc) for b in branches: branch_url_args = dict(url_args) if path: b['path'] = path branch_url_args['path'] = path b['url'] = reverse('browse-origin-directory', kwargs=branch_url_args, query_params={'branch': b['name']}) path_info = gen_path_info(path) breadcrumbs = [] breadcrumbs.append({'name': root_sha1_git[:7], 'url': reverse('browse-origin-directory', kwargs=url_args, query_params={'branch': branch})}) for pi in path_info: bc_url_args = dict(url_args) bc_url_args['path'] = pi['path'] breadcrumbs.append({'name': pi['name'], 'url': reverse('browse-origin-directory', kwargs=bc_url_args, query_params={'branch': branch})}) path = '' if path is None else (path + '/') for d in dirs: bc_url_args = dict(url_args) bc_url_args['path'] = path + d['name'] d['url'] = reverse('browse-origin-directory', kwargs=bc_url_args, query_params={'branch': branch}) for f in files: bc_url_args = dict(url_args) bc_url_args['path'] = path + f['name'] f['url'] = reverse('browse-origin-content', kwargs=bc_url_args, query_params={'branch': branch}) return render(request, 'directory.html', {'dir_sha1_git': sha1_git, 'dirs': dirs, 'files': files, 'breadcrumbs': breadcrumbs, 'branches': branches, 'branch': branch}) @browse_route(r'origin/(?P[0-9]+)/content/(?P.+)/', r'origin/(?P[0-9]+)/visit/(?P[0-9]+)/content/(?P.+)/', # noqa r'origin/(?P[0-9]+)/ts/(?P[0-9]+)/content/(?P.+)/', # noqa view_name='browse-origin-content') def origin_content_display(request, origin_id, path, visit_id=None, ts=None): """Django view that produces an HTML display of a swh content associated to an origin for a given visit. The url scheme that points to it is the following: * :http:get:`/browse/origin/(origin_id)/content/(path)/` * :http:get:`/browse/origin/(origin_id)/visit/(visit_id)/content/(path)/` * :http:get:`/browse/origin/(origin_id)/ts/(ts)/content/(path)/` Args: request: input django http request origin_id: id of a swh origin path: path of the content relative to the origin root directory visit_id: optionnal visit id parameter (the last one will be used by default) ts: optionnal visit timestamp parameter (the last one will be used by default) branch: optionnal query parameter that specifies the origin branch from which to retrieve the content Returns: The HTML rendering of the requested content associated to the provided origin and visit. """ # noqa try: if not visit_id and not ts: origin_visits = get_origin_visits(origin_id) return origin_content_display(request, origin_id, path, origin_visits[-1]['visit']) if not visit_id and ts: branches = get_origin_visit_branches(origin_id, visit_ts=ts) kwargs = {'origin_id': origin_id, 'ts': ts} else: branches = get_origin_visit_branches(origin_id, visit_id) kwargs = {'origin_id': origin_id, 'visit_id': visit_id} for b in branches: bc_kwargs = dict(kwargs) bc_kwargs['path'] = path b['url'] = reverse('browse-origin-content', kwargs=bc_kwargs, query_params={'branch': b['name']}) branch = request.GET.get('branch', 'master') filtered_branches = [b for b in branches if branch in b['name']] if len(filtered_branches) > 0: root_sha1_git = filtered_branches[0]['directory'] branch = filtered_branches[0]['name'] else: if visit_id: raise NotFoundExc('Branch %s associated to visit with' ' id %s for origin with id %s' ' not found!' % (branch, visit_id, origin_id)) else: raise NotFoundExc('Branch %s associated to visit with' ' timestamp %s for origin with id %s' ' not found!' % (branch, ts, origin_id)) content_info = service.lookup_directory_with_path(root_sha1_git, path) sha1_git = content_info['target'] query_string = 'sha1_git:' + sha1_git content_data, mime_type = request_content(query_string) except Exception as exc: return handle_view_exception(exc) content_display_data = prepare_content_for_display(content_data, mime_type, path) filename = None path_info = None breadcrumbs = [] split_path = path.split('/') filename = split_path[-1] path = path.replace(filename, '') path_info = gen_path_info(path) breadcrumbs.append({'name': root_sha1_git[:7], 'url': reverse('browse-origin-directory', kwargs=kwargs, query_params={'branch': branch})}) for pi in path_info: bc_kwargs = dict(kwargs) bc_kwargs['path'] = pi['path'] breadcrumbs.append({'name': pi['name'], 'url': reverse('browse-origin-directory', kwargs=bc_kwargs, query_params={'branch': branch})}) breadcrumbs.append({'name': filename, 'url': None}) content_raw_url = reverse('browse-content-raw', kwargs={'query_string': query_string}, query_params={'filename': filename}) return render(request, 'content.html', {'content_hash_algo': 'sha1_git', 'content_checksum': sha1_git, 'content': content_display_data['content_data'], 'content_raw_url': content_raw_url, 'mime_type': mime_type, 'language': content_display_data['language'], 'breadcrumbs': breadcrumbs, 'branches': branches, 'branch': branch}) diff --git a/swh/web/browse/views/revision.py b/swh/web/browse/views/revision.py new file mode 100644 index 00000000..f8b7ad16 --- /dev/null +++ b/swh/web/browse/views/revision.py @@ -0,0 +1,176 @@ +# Copyright (C) 2017 The Software Heritage developers +# See the AUTHORS file at the top-level directory of this distribution +# License: GNU General Public License version 3, or any later version +# See top-level LICENSE file for more information + +import textwrap + +from django.shortcuts import render +from django.utils.safestring import mark_safe +from swh.web.common import service +from swh.web.common.utils import reverse, format_utc_iso_date +from swh.web.common.exc import handle_view_exception +from swh.web.browse.browseurls import browse_route + + +def _gen_person_link(person_id, person_name): + person_url = reverse('browse-person', kwargs={'person_id': person_id}) + person_link = '%s' % (person_url, person_name) + return mark_safe(person_link) + + +def _gen_directory_link(sha1_git, link_text): + directory_url = reverse('browse-directory', + kwargs={'sha1_git': sha1_git}) + directory_link = '%s' % \ + (directory_url, link_text) + return mark_safe(directory_link) + + +def _gen_revision_link(revision_id, shorten_id=False): + revision_url = reverse('browse-revision', + kwargs={'sha1_git': revision_id}) + if shorten_id: + revision_link = '%s' % (revision_url, revision_id[:7]) + else: + revision_link = '%s' % (revision_url, revision_id) + return mark_safe(revision_link) + + +def _gen_revision_log_link(revision_id): + revision_log_url = reverse('browse-revision-log', + kwargs={'sha1_git': revision_id}) + revision_log_link = '%s' % (revision_log_url, + revision_log_url) + return mark_safe(revision_log_link) + + +@browse_route(r'revision/(?P[0-9a-f]+)/', + view_name='browse-revision') +def revision_browse(request, sha1_git): + """ + Django view that produces an HTML display of a SWH revision + identified by its id. + + The url that points to it is :http:get:`/browse/revision/(revision_id)/`. + + Args: + request: input django http request + sha1_git: a SWH revision id + + Returns: + The HMTL rendering for the metadata of the provided revision. + """ + try: + revision = service.lookup_revision(sha1_git) + except Exception as exc: + return handle_view_exception(exc) + + revision_data = {} + + revision_data['author'] = _gen_person_link( + revision['author']['id'], revision['author']['name']) + revision_data['committer'] = _gen_person_link( + revision['committer']['id'], revision['committer']['name']) + revision_data['committer date'] = format_utc_iso_date( + revision['committer_date']) + revision_data['date'] = format_utc_iso_date(revision['date']) + revision_data['directory'] = _gen_directory_link(revision['directory'], + revision['directory']) + revision_data['history log'] = _gen_revision_log_link(sha1_git) + revision_data['merge'] = revision['merge'] + revision_data['message'] = revision['message'] + + parents = '' + for p in revision['parents']: + parent_link = _gen_revision_link(p) + parents += parent_link + '
' + + revision_data['parents'] = mark_safe(parents) + revision_data['synthetic'] = revision['synthetic'] + revision_data['type'] = revision['type'] + + return render(request, 'revision.html', {'revision': revision_data}) + + +NB_LOG_ENTRIES = 20 + + +@browse_route(r'revision/(?P[0-9a-f]+)/log/', + view_name='browse-revision-log') +def revision_log_browse(request, sha1_git): + """ + Django view that produces an HTML display of the history + log for a SWH revision identified by its id. + + The url that points to it is :http:get:`/browse/revision/(revision_id)/log/`. + + Args: + request: input django http request + sha1_git: a SWH revision id + + Returns: + The HMTL rendering of the revision history log. + """ # noqa + try: + per_page = int(request.GET.get('per_page', NB_LOG_ENTRIES)) + revision_log = service.lookup_revision_log(sha1_git, + limit=per_page+1) + revision_log = list(revision_log) + except Exception as exc: + return handle_view_exception(exc) + + revision_log_data = [] + + next_rev = None + prev_rev = None + next_revs_breadcrumb = None + prev_revs_breadcrumb = None + if len(revision_log) == per_page + 1: + prev_rev = revision_log[-1]['id'] + + revs_breadcrumb = request.GET.get('revs_breadcrumb', None) + if revs_breadcrumb: + revs = revs_breadcrumb.split('/') + next_rev = revs[-1] + if len(revs) > 1: + next_revs_breadcrumb = '/'.join(revs[:-1]) + prev_revs_breadcrumb = revs_breadcrumb + '/' + sha1_git + else: + prev_revs_breadcrumb = sha1_git + + prev_rev_url = None + if prev_rev: + prev_rev_url = \ + reverse('browse-revision-log', + kwargs={'sha1_git': prev_rev}, + query_params={'revs_breadcrumb': prev_revs_breadcrumb, + 'per_page': per_page}) + + next_rev_url = None + if next_rev: + next_rev_url = \ + reverse('browse-revision-log', + kwargs={'sha1_git': next_rev}, + query_params={'revs_breadcrumb': next_revs_breadcrumb, + 'per_page': per_page}) + + for i, log in enumerate(revision_log): + if i == per_page: + break + revision_log_data.append({ + 'author': _gen_person_link(log['author']['id'], + log['author']['name']), + 'revision': _gen_revision_link(log['id'], True), + 'message': log['message'], + 'message_shorten': textwrap.shorten(log['message'], + width=80, + placeholder='...'), + 'date': format_utc_iso_date(log['date']), + 'directory': _gen_directory_link(log['directory'], 'Tree') + }) + + return render(request, 'revision-log.html', + {'revision_log': revision_log_data, + 'next_rev_url': next_rev_url, + 'prev_rev_url': prev_rev_url}) diff --git a/swh/web/common/utils.py b/swh/web/common/utils.py index 39dd3662..b1148e94 100644 --- a/swh/web/common/utils.py +++ b/swh/web/common/utils.py @@ -1,120 +1,143 @@ # Copyright (C) 2017 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information import re from datetime import datetime, timezone -from dateutil import parser +from dateutil import parser as date_parser from swh.web.common.exc import BadInputExc import urllib from django.core import urlresolvers from django.http import QueryDict # override django reverse function in order to get # the same result on debian jessie and stretch # (see https://code.djangoproject.com/ticket/22223) def reverse(viewname, args=None, kwargs=None, query_params=None, current_app=None, urlconf=None): """An override of django reverse function supporting multiple django versions (from 1.7 to current) and query parameters. Args: viewname: the name of the django view from which to compute a url args: list of url arguments ordered according to their position it kwargs: dictionnary of url arguments indexed by their names query_params: dictionnary of query parameters to append to the reversed url current_app: the name of the django app tighted to the view urlconf: url configuration module Returns: The url of the requested view with processed arguments and query parameters """ url = urllib.parse.unquote( urlresolvers.reverse( viewname, urlconf=urlconf, args=args, kwargs=kwargs, current_app=current_app ) ) + + if query_params: + query_params = {k: v for k, v in query_params.items() if v is not None} + if query_params and len(query_params) > 0: query_dict = QueryDict('', mutable=True) for k, v in query_params.items(): query_dict[k] = v url += ('?' + query_dict.urlencode(safe='/')) + return url def fmap(f, data): """Map f to data at each level. This must keep the origin data structure type: - map -> map - dict -> dict - list -> list - None -> None Args: f: function that expects one argument. data: data to traverse to apply the f function. list, map, dict or bare value. Returns: The same data-structure with modified values by the f function. """ if data is None: return data if isinstance(data, map): return map(lambda y: fmap(f, y), (x for x in data)) if isinstance(data, list): return [fmap(f, x) for x in data] if isinstance(data, dict): return {k: fmap(f, v) for (k, v) in data.items()} return f(data) def parse_timestamp(timestamp): """Given a time or timestamp (as string), parse the result as datetime. Returns: a timezone-aware datetime representing the parsed value. None if the parsing fails. Samples: - 2016-01-12 - 2016-01-12T09:19:12+0100 - Today is January 1, 2047 at 8:21:00AM - 1452591542 """ if not timestamp: return None try: - return parser.parse(timestamp, ignoretz=False, fuzzy=True) + return date_parser.parse(timestamp, ignoretz=False, fuzzy=True) except: try: return datetime.utcfromtimestamp(float(timestamp)).replace( tzinfo=timezone.utc) except (ValueError, OverflowError) as e: raise BadInputExc(e) def shorten_path(path): """Shorten the given path: for each hash present, only return the first 8 characters followed by an ellipsis""" sha256_re = r'([0-9a-f]{8})[0-9a-z]{56}' sha1_re = r'([0-9a-f]{8})[0-9a-f]{32}' ret = re.sub(sha256_re, r'\1...', path) return re.sub(sha1_re, r'\1...', ret) + + +def format_utc_iso_date(iso_date): + """Turns a string reprensation of an UTC iso date + into a more human readable one. + + More precisely, from the following input + string: '2017-05-04T13:27:13+02:00' the following one + is returned: '04 May 2017, 13:27 UTC'. + + Args: + iso_date (str): a string representation of an UTC iso date + + Returns: + A human readable string representation of the input iso date + """ + date = date_parser.parse(iso_date) + return date.strftime('%d %B %Y, %H:%M UTC') diff --git a/swh/web/templates/revision-log.html b/swh/web/templates/revision-log.html index 87751e46..9e83c371 100644 --- a/swh/web/templates/revision-log.html +++ b/swh/web/templates/revision-log.html @@ -1,156 +1,43 @@ {% extends "layout.html" %} {% block title %}Revision Log{% endblock %} {% block content %} -{% if message is not none %} -
- {{ message }} -
-{% endif %} - -
-

Queried revision:

- {% if sha1_git is not none %} -
Revision with git SHA1 {{ sha1_git }}
- {% else %} - -
Branch name {{ branch_name }}
- {% if timestamp is not none %} -
Time stamp {{ timestamp }}
- {% endif %} - {% endif %} -
- -{% if revisions is not none %} -
- {% for revision in revisions %} - {% if revision['merge'] %} -
-
- Merge -
-
- {% for url in revision['parent_urls'] %} - {{ url | revision_id_from_url }} +

Revision log

+ + + + + + + + + + + + + {% for log in revision_log %} + + + + + + + {% endfor %} - - - {% endif %} - - {% if revision['url'] is not none %} -
-
Revision
- -
- {% endif %} - - {% if revision['history_url'] is not none %} -
-
Revision Log
- -
- {% endif %} - - {% if revision['history_context_url'] is not none %} -
-
Contextual Revision Log
- -
- {% endif %} + +
AuthorRevisionMessageDate
{{ log.author }}{{ log.revision }}{{ log.message_shorten }}{{ log.date }}{{ log.directory }}
- {% if revision['directory_url'] is not none %} - - {% endif %} - - {% if revision['author'] is not none %} -
-
Author
-
-

- {{ revision['author']['name'] }} - {% if 'decoding_failures' in revision['author'] %}(some decoding errors){% endif %} -

-
-
-
-
Date
-

{{ revision['date'] }}

-
- {% endif %} - - {% if revision['committer'] is not none %} -
-
Committer
-
-

- {{ revision['committer']['name'] }} - {% if 'decoding_failures' in revision['committer'] %}(some decoding errors){% endif %} -

-
-
-
-
Committer Date
-

{{ revision['committer_date'] }}

-
- {% endif %} - - {% if revision['message'] is not none %} -
-
Message
-
{{ revision['message'] }}
-
- {% elif revision['message_encoding_failed'] %} -
-
Message
- -
+
    + {% if next_rev_url %} +
  • Newer
  • {% else %} -
    -
    Message
    -
    No message found.
    -
    +
  • Newer
  • {% endif %} - - {% for key in revision.keys() %} - {% if key in ['type', 'synthetic'] and key not in ['decoding_failures'] and revision[key] is not none %} -
    -
    {{ key }}
    -
    {{ revision[key] }}
    -
    + {% if prev_rev_url %} +
  • Older
  • + {% else %} +
  • Older
  • {% endif %} - {% endfor %} +
- {% for key in ['children_urls', 'parent_urls'] %} - {% if revision[key] is not none %} -
-
{{ key }}
- {% for link in revision[key] %} - - {% endfor %} -
- {% endif %} - {% endfor %} - - {% if 'decoding_failures' in revision %} -
-
(some decoding errors occurred)
-
- {% endif %} -
- {% endfor %} - - {% if next_revs_url is not none %} - - - Next revisions - - - - {% endif %} - {% endif %} -
-
{% endblock %} diff --git a/swh/web/templates/revision.html b/swh/web/templates/revision.html index 8629aa1d..29f74214 100644 --- a/swh/web/templates/revision.html +++ b/swh/web/templates/revision.html @@ -1,116 +1,17 @@ {% extends "layout.html" %} +{% load swh_templatetags %} {% block title %}Revision{% endblock %} {% block content %} -{% if message is not none %} - {{ message }} -{% endif %} +

Revision information

-{% if revision is not none %} -
- {% if revision['url'] is not none %} -
-
Revision
- -
- {% endif %} - - {% if revision['history_url'] is not none %} -
-
Revision Log
- -
- {% endif %} - - {% if revision['history_context_url'] is not none %} -
-
Contextual Revision Log
- -
- {% endif %} - - {% if revision['directory_url'] is not none %} - - {% endif %} - - {% if revision['author'] is not none %} -
-
Author
-
-

- {{ revision['author']['name'] }} - {% if 'decoding_failures' in revision['author'] %}(some decoding failed){% endif %} -

-
-
-
-
Date
-

{{ revision['date'] }}

-
- {% endif %} - - {% if revision['committer'] is not none %} -
-
Committer
-
-

- {{ revision['committer']['name'] }} - {% if 'decoding_failures' in revision['committer'] %}(some decoding failed){% endif %} -

-
-
-
-
Committer Date
-

{{ revision['committer_date'] }}

-
- {% endif %} - - {% if revision['message'] is not none %} -
-
Message
-
{{ revision['message'] }}
-
- {% elif revision['message_encoding_failed'] %} -
-
Message
- -
-
Message
-
No message found.
-
- {% endif %} - - {% for key in revision.keys() %} - {% if key in ['type', 'synthetic'] and revision[key] is not none %} -
-
{{ key }}
-

{{ revision[key] }}

-
- {% endif %} - {% endfor %} - - {% for key in ['parent_urls', 'children_urls'] %} - {% if revision[key] is not none %} -
-
{{ key }}
- {% for link in revision[key] %} - - {% endfor %} -
- {% endif %} - {% endfor %} - {% if 'decoding_failures' in revision %} -
-
(some decoding failed)
-
- {% endif %} - -
-{% endif %} + + {% for key, val in revision.items|dictsort:0 %} + + + + + {% endfor %} +
{{ key }}{{ val }}
{% endblock %} diff --git a/swh/web/tests/browse/views/data/revision_test_data.py b/swh/web/tests/browse/views/data/revision_test_data.py new file mode 100644 index 00000000..6efbe101 --- /dev/null +++ b/swh/web/tests/browse/views/data/revision_test_data.py @@ -0,0 +1,859 @@ +# Copyright (C) 2017 The Software Heritage developers +# See the AUTHORS file at the top-level directory of this distribution +# License: GNU General Public License version 3, or any later version +# See top-level LICENSE file for more information + +# flake8: noqa + +revision_id_test = '7bc08e1aa0b08cb23e18715a32aa38517ad34672' + +revision_metadata_test = \ +{'id': '7bc08e1aa0b08cb23e18715a32aa38517ad34672', + 'type': 'git', + 'parents': ['bf3652b16b65c27db5243aa0d674e2de4a8ccde9', + 'a952bb99a6830804d06c5b8e04b75c66100fbae9'], + 'metadata': {}, + 'committer': {'name': 'GitHub', + 'fullname': 'GitHub ', + 'id': 10932771, + 'email': 'noreply@github.com'}, + 'directory': 'ae59ceecf46367e8e4ad800e231fc76adc3afffb', + 'date': '2017-05-04T13:27:13+02:00', + 'merge': True, + 'committer_date': '2017-05-04T13:27:13+02:00', + 'author': {'name': 'Tobias Koppers', + 'fullname': 'Tobias Koppers ', + 'id': 141959, + 'email': 'tobias.koppers@googlemail.com'}, + 'message': 'Merge pull request #4816 from webpack/bugfix/hoist-immutable-export\n\nhoist exports', + 'synthetic': False +} + +revision_history_log_test = \ +[{'author': {'email': 'tobias.koppers@googlemail.com', + 'fullname': 'Tobias Koppers ', + 'id': 141959, + 'name': 'Tobias Koppers'}, + 'committer': {'email': 'noreply@github.com', + 'fullname': 'GitHub ', + 'id': 10932771, + 'name': 'GitHub'}, + 'committer_date': '2017-05-04T13:27:13+02:00', + 'date': '2017-05-04T13:27:13+02:00', + 'directory': 'ae59ceecf46367e8e4ad800e231fc76adc3afffb', + 'id': '7bc08e1aa0b08cb23e18715a32aa38517ad34672', + 'merge': True, + 'message': 'Merge pull request #4816 from ' + 'webpack/bugfix/hoist-immutable-export\n' + '\n' + 'hoist exports', + 'metadata': {}, + 'parents': ['bf3652b16b65c27db5243aa0d674e2de4a8ccde9', + 'a952bb99a6830804d06c5b8e04b75c66100fbae9'], + 'synthetic': False, + 'type': 'git'}, + {'author': {'email': 'tobias.koppers@googlemail.com', + 'fullname': 'Tobias Koppers ', + 'id': 141959, + 'name': 'Tobias Koppers'}, + 'committer': {'email': 'tobias.koppers@googlemail.com', + 'fullname': 'Tobias Koppers ', + 'id': 141959, + 'name': 'Tobias Koppers'}, + 'committer_date': '2017-05-04T12:17:13+02:00', + 'date': '2017-05-04T12:17:13+02:00', + 'directory': '344f6b38f021fa0bbd60ca06fe1cbc61164e7abe', + 'id': 'bf3652b16b65c27db5243aa0d674e2de4a8ccde9', + 'merge': False, + 'message': '2.5.0\n', + 'metadata': {}, + 'parents': ['cd1cd29fba46bd0133db0ca89acbe6c6c0240323'], + 'synthetic': False, + 'type': 'git'}, + {'author': {'email': 'tobias.koppers@googlemail.com', + 'fullname': 'Tobias Koppers ', + 'id': 141959, + 'name': 'Tobias Koppers'}, + 'committer': {'email': 'tobias.koppers@googlemail.com', + 'fullname': 'Tobias Koppers ', + 'id': 141959, + 'name': 'Tobias Koppers'}, + 'committer_date': '2017-05-04T13:00:52+02:00', + 'date': '2017-05-04T12:56:31+02:00', + 'directory': 'ae59ceecf46367e8e4ad800e231fc76adc3afffb', + 'id': 'a952bb99a6830804d06c5b8e04b75c66100fbae9', + 'merge': False, + 'message': 'change some magic numbers to hoist exports\n\nfixes #4753\n', + 'metadata': {}, + 'parents': ['bf3652b16b65c27db5243aa0d674e2de4a8ccde9'], + 'synthetic': False, + 'type': 'git'}, + {'author': {'email': 'tobias.koppers@googlemail.com', + 'fullname': 'Tobias Koppers ', + 'id': 141959, + 'name': 'Tobias Koppers'}, + 'committer': {'email': 'noreply@github.com', + 'fullname': 'GitHub ', + 'id': 10932771, + 'name': 'GitHub'}, + 'committer_date': '2017-05-04T11:41:43+02:00', + 'date': '2017-05-04T11:41:43+02:00', + 'directory': 'fbc01dab452f80bf49d554cc920979a66839707f', + 'id': 'cd1cd29fba46bd0133db0ca89acbe6c6c0240323', + 'merge': True, + 'message': 'Merge pull request #4815 from ' + 'webpack/bugfix/extract-async-initial\n' + '\n' + "CommonsChunkPlugin in async mode doesn't select initial chunks", + 'metadata': {}, + 'parents': ['8bab88c50fcb87c749c244b9ab28a1fb7e173bea', + 'b45588bc1197abbc309eb3705a4bf89960b001ae'], + 'synthetic': False, + 'type': 'git'}, + {'author': {'email': 'tobias.koppers@googlemail.com', + 'fullname': 'Tobias Koppers ', + 'id': 141959, + 'name': 'Tobias Koppers'}, + 'committer': {'email': 'noreply@github.com', + 'fullname': 'GitHub ', + 'id': 10932771, + 'name': 'GitHub'}, + 'committer_date': '2017-05-04T10:50:03+02:00', + 'date': '2017-05-04T10:50:03+02:00', + 'directory': '74ed598c6b831fe7f68697f6ed761660a842b973', + 'id': '8bab88c50fcb87c749c244b9ab28a1fb7e173bea', + 'merge': True, + 'message': 'Merge pull request #4814 from webpack/test/move-entry\n' + '\n' + 'add testcase for moving entry modules into the commons chunk', + 'metadata': {}, + 'parents': ['85dc98f17aa39d5d3337e3791bf25634a1f7e445', + 'a244879a07e04e6b5951520ca3cd80c3ef160f8e'], + 'synthetic': False, + 'type': 'git'}, + {'author': {'email': 'tobias.koppers@googlemail.com', + 'fullname': 'Tobias Koppers ', + 'id': 141959, + 'name': 'Tobias Koppers'}, + 'committer': {'email': 'tobias.koppers@googlemail.com', + 'fullname': 'Tobias Koppers ', + 'id': 141959, + 'name': 'Tobias Koppers'}, + 'committer_date': '2017-05-04T10:50:46+02:00', + 'date': '2017-05-04T10:50:46+02:00', + 'directory': '735b9f6d54d62470944c4743d52931b308a9072c', + 'id': 'b45588bc1197abbc309eb3705a4bf89960b001ae', + 'merge': False, + 'message': "CommonsChunkPlugin in async mode doesn't select initial chunks\n" + '\n' + 'fixes #4795\n', + 'metadata': {}, + 'parents': ['c91ba4949503de5ca9d98c98188c2654b095f2cb'], + 'synthetic': False, + 'type': 'git'}, + {'author': {'email': 'tobias.koppers@googlemail.com', + 'fullname': 'Tobias Koppers ', + 'id': 141959, + 'name': 'Tobias Koppers'}, + 'committer': {'email': 'noreply@github.com', + 'fullname': 'GitHub ', + 'id': 10932771, + 'name': 'GitHub'}, + 'committer_date': '2017-05-04T08:04:41+02:00', + 'date': '2017-05-04T08:04:41+02:00', + 'directory': 'c3c24aeafabe1d441c37a2769e9b65ae10075925', + 'id': '85dc98f17aa39d5d3337e3791bf25634a1f7e445', + 'merge': True, + 'message': 'Merge pull request #4813 from JLHwung/perf/date-now\n' + '\n' + 'Perf/use Date.now() instead of +new Date()/new Date().getTime()', + 'metadata': {}, + 'parents': ['c91ba4949503de5ca9d98c98188c2654b095f2cb', + '6afc397b99aef338a9d66add4488ce03ea3f7a43'], + 'synthetic': False, + 'type': 'git'}, + {'author': {'email': 'tobias.koppers@googlemail.com', + 'fullname': 'Tobias Koppers ', + 'id': 141959, + 'name': 'Tobias Koppers'}, + 'committer': {'email': 'tobias.koppers@googlemail.com', + 'fullname': 'Tobias Koppers ', + 'id': 141959, + 'name': 'Tobias Koppers'}, + 'committer_date': '2017-05-04T10:01:04+02:00', + 'date': '2017-05-04T10:01:04+02:00', + 'directory': '4de09be1628ed81def03f78d8d832c93efdf0af4', + 'id': 'a244879a07e04e6b5951520ca3cd80c3ef160f8e', + 'merge': False, + 'message': 'add testcase for moving entry modules into the commons chunk\n' + '#4795\n', + 'metadata': {}, + 'parents': ['c91ba4949503de5ca9d98c98188c2654b095f2cb'], + 'synthetic': False, + 'type': 'git'}, + {'author': {'email': 'tobias.koppers@googlemail.com', + 'fullname': 'Tobias Koppers ', + 'id': 141959, + 'name': 'Tobias Koppers'}, + 'committer': {'email': 'noreply@github.com', + 'fullname': 'GitHub ', + 'id': 10932771, + 'name': 'GitHub'}, + 'committer_date': '2017-04-30T09:31:17+02:00', + 'date': '2017-04-30T09:31:17+02:00', + 'directory': '16c9d449871efccef7a0f53b29bd6218cc769e30', + 'id': 'c91ba4949503de5ca9d98c98188c2654b095f2cb', + 'merge': True, + 'message': 'Merge pull request #4791 from deificx/master\n' + '\n' + 'add option to lib/Stats.js to disable stack trace on errors and ' + 'warnings', + 'metadata': {}, + 'parents': ['94ba75f7940836390c041846f2c334929ee14332', + '84ea1ffd3d0892f3356ac0494947bbc2a0e39d51'], + 'synthetic': False, + 'type': 'git'}, + {'author': {'email': 'i@jhuang.me', + 'fullname': 'J Huang ', + 'id': 12072398, + 'name': 'J Huang'}, + 'committer': {'email': 'i@jhuang.me', + 'fullname': 'J Huang ', + 'id': 12072398, + 'name': 'J Huang'}, + 'committer_date': '2017-05-04T10:19:54+08:00', + 'date': '2017-05-04T10:19:54+08:00', + 'directory': 'c3c24aeafabe1d441c37a2769e9b65ae10075925', + 'id': '6afc397b99aef338a9d66add4488ce03ea3f7a43', + 'merge': False, + 'message': 'perf: use Date.now() instead of new Date().getTime()\n' + '\n' + 'new Date().getTime() is 2x slower than Date.now(), see ' + 'https://jsperf.com/new-date-vs-date-now-vs-performance-now/6\n', + 'metadata': {}, + 'parents': ['94d0641ba40d65d5fcbd64414b6ea9f8a2589538'], + 'synthetic': False, + 'type': 'git'}, + {'author': {'email': 'tobias.koppers@googlemail.com', + 'fullname': 'Tobias Koppers ', + 'id': 141959, + 'name': 'Tobias Koppers'}, + 'committer': {'email': 'noreply@github.com', + 'fullname': 'GitHub ', + 'id': 10932771, + 'name': 'GitHub'}, + 'committer_date': '2017-04-30T09:30:03+02:00', + 'date': '2017-04-30T09:30:03+02:00', + 'directory': 'f78fe6939706d761f0bc60f9d566bd414ef5c113', + 'id': '94ba75f7940836390c041846f2c334929ee14332', + 'merge': True, + 'message': 'Merge pull request #4794 from ' + 'ndresx/disable-manifest-json-pretty-print\n' + '\n' + 'Disable manifest.json pretty print', + 'metadata': {}, + 'parents': ['24ef6ea1b56b358caeb4b07476a909f4f86c2d8a', + 'de87f93c1b050db59ffbeff3aed4ac3b3eb57da3'], + 'synthetic': False, + 'type': 'git'}, + {'author': {'email': 'even.alander@videxio.com', + 'fullname': 'Even Alander ', + 'id': 3407016, + 'name': 'Even Alander'}, + 'committer': {'email': 'even.alander@videxio.com', + 'fullname': 'Even Alander ', + 'id': 3407016, + 'name': 'Even Alander'}, + 'committer_date': '2017-04-29T20:53:42+02:00', + 'date': '2017-04-29T20:52:43+02:00', + 'directory': '68fcb2d3c60aa32b708ce9ca9fa5c4b1f7a2532d', + 'id': '84ea1ffd3d0892f3356ac0494947bbc2a0e39d51', + 'merge': False, + 'message': 'added error to stats.moduleTrace test name to trigger test cases ' + 'corretly\n', + 'metadata': {}, + 'parents': ['8ad4386bdf9cd607310a4255dff8cb9ccd12afad'], + 'synthetic': False, + 'type': 'git'}, + {'author': {'email': 'i@jhuang.me', + 'fullname': 'J Huang ', + 'id': 12072398, + 'name': 'J Huang'}, + 'committer': {'email': 'i@jhuang.me', + 'fullname': 'J Huang ', + 'id': 12072398, + 'name': 'J Huang'}, + 'committer_date': '2017-05-04T10:19:10+08:00', + 'date': '2017-05-04T10:17:23+08:00', + 'directory': '41965bffa3f73c8928beb2a6580c253adb9bd5cf', + 'id': '94d0641ba40d65d5fcbd64414b6ea9f8a2589538', + 'merge': False, + 'message': 'perf: use Date.now() instead of +new Date()\n' + '\n' + '+new Date() is 2x slower than Date.now(), see ' + 'https://jsperf.com/new-date-vs-date-now-vs-performance-now/6\n', + 'metadata': {}, + 'parents': ['c91ba4949503de5ca9d98c98188c2654b095f2cb'], + 'synthetic': False, + 'type': 'git'}, + {'author': {'email': 'tobias.koppers@googlemail.com', + 'fullname': 'Tobias Koppers ', + 'id': 141959, + 'name': 'Tobias Koppers'}, + 'committer': {'email': 'noreply@github.com', + 'fullname': 'GitHub ', + 'id': 10932771, + 'name': 'GitHub'}, + 'committer_date': '2017-04-26T09:05:56+02:00', + 'date': '2017-04-26T09:05:56+02:00', + 'directory': 'aa78629e4aefc836b5a967db57cac29f79fea074', + 'id': '24ef6ea1b56b358caeb4b07476a909f4f86c2d8a', + 'merge': True, + 'message': 'Merge pull request #4779 from ndresx/ignoreplugin-typo-fix\n' + '\n' + 'Fix typo in IgnorePlugin', + 'metadata': {}, + 'parents': ['34315db3ffe20b3d7e9a50885ea623fc90ac0861', + 'aa172f0a359ab3c0b0987101c64c8b41b07d581f'], + 'synthetic': False, + 'type': 'git'}, + {'author': {'email': 'ndresx@gmail.com', + 'fullname': 'Martin Veith ', + 'id': 14855385, + 'name': 'Martin Veith'}, + 'committer': {'email': 'ndresx@gmail.com', + 'fullname': 'Martin Veith ', + 'id': 14855385, + 'name': 'Martin Veith'}, + 'committer_date': '2017-04-28T21:34:38+02:00', + 'date': '2017-04-28T21:34:38+02:00', + 'directory': 'f78fe6939706d761f0bc60f9d566bd414ef5c113', + 'id': 'de87f93c1b050db59ffbeff3aed4ac3b3eb57da3', + 'merge': False, + 'message': 'Disable manifest.json pretty print\n', + 'metadata': {}, + 'parents': ['24ef6ea1b56b358caeb4b07476a909f4f86c2d8a'], + 'synthetic': False, + 'type': 'git'}, + {'author': {'email': 'even.alander@videxio.com', + 'fullname': 'Even Alander ', + 'id': 3407016, + 'name': 'Even Alander'}, + 'committer': {'email': 'even.alander@videxio.com', + 'fullname': 'Even Alander ', + 'id': 3407016, + 'name': 'Even Alander'}, + 'committer_date': '2017-04-29T19:43:45+02:00', + 'date': '2017-04-29T19:43:45+02:00', + 'directory': '893a65a9ad785d719c44a3e764d150bb47cba6c1', + 'id': '8ad4386bdf9cd607310a4255dff8cb9ccd12afad', + 'merge': False, + 'message': 'test cases for stats.moduleTrace option\n', + 'metadata': {}, + 'parents': ['958156ae4201e75f8cb04e306d2a38b94d2a8a1d'], + 'synthetic': False, + 'type': 'git'}, + {'author': {'email': 'tobias.koppers@googlemail.com', + 'fullname': 'Tobias Koppers ', + 'id': 141959, + 'name': 'Tobias Koppers'}, + 'committer': {'email': 'noreply@github.com', + 'fullname': 'GitHub ', + 'id': 10932771, + 'name': 'GitHub'}, + 'committer_date': '2017-04-23T14:25:42+02:00', + 'date': '2017-04-23T14:25:42+02:00', + 'directory': 'f511a1febf3d6b9b788080f3eb69ff3cb963758f', + 'id': '34315db3ffe20b3d7e9a50885ea623fc90ac0861', + 'merge': True, + 'message': 'Merge pull request #3875 from webpack/test/circleci\n' + '\n' + 'Update circle.yml', + 'metadata': {}, + 'parents': ['6a26e9ba7f7f1be7e76054f219bf1e094f2c3264', + '0f91f949e2d41ef5cb92493bcc6c1fa7578ac27d'], + 'synthetic': False, + 'type': 'git'}, + {'author': {'email': 'ndresx@gmail.com', + 'fullname': 'Martin Veith ', + 'id': 14855385, + 'name': 'Martin Veith'}, + 'committer': {'email': 'ndresx@gmail.com', + 'fullname': 'Martin Veith ', + 'id': 14855385, + 'name': 'Martin Veith'}, + 'committer_date': '2017-04-25T22:07:43+02:00', + 'date': '2017-04-25T22:07:43+02:00', + 'directory': 'aa78629e4aefc836b5a967db57cac29f79fea074', + 'id': 'aa172f0a359ab3c0b0987101c64c8b41b07d581f', + 'merge': False, + 'message': 'Fix typo in IgnorePlugin\n', + 'metadata': {}, + 'parents': ['34315db3ffe20b3d7e9a50885ea623fc90ac0861'], + 'synthetic': False, + 'type': 'git'}, + {'author': {'email': 'even.alander@videxio.com', + 'fullname': 'Even Alander ', + 'id': 3407016, + 'name': 'Even Alander'}, + 'committer': {'email': 'even.alander@videxio.com', + 'fullname': 'Even Alander ', + 'id': 3407016, + 'name': 'Even Alander'}, + 'committer_date': '2017-04-29T19:43:15+02:00', + 'date': '2017-04-29T19:43:15+02:00', + 'directory': '9d7347c42e29afda2849996ca6b61cdc7ea1b5a1', + 'id': '958156ae4201e75f8cb04e306d2a38b94d2a8a1d', + 'merge': False, + 'message': 'moduleTrace added to webpackOptionsSchema.json\n', + 'metadata': {}, + 'parents': ['41310135bb4e30a0a6f71eddd3d74419ec8512a7'], + 'synthetic': False, + 'type': 'git'}, + {'author': {'email': 'tobias.koppers@googlemail.com', + 'fullname': 'Tobias Koppers ', + 'id': 141959, + 'name': 'Tobias Koppers'}, + 'committer': {'email': 'noreply@github.com', + 'fullname': 'GitHub ', + 'id': 10932771, + 'name': 'GitHub'}, + 'committer_date': '2017-04-23T12:57:02+02:00', + 'date': '2017-04-23T12:57:02+02:00', + 'directory': '71848e66952829c98ec4e57e2338be986390b850', + 'id': '6a26e9ba7f7f1be7e76054f219bf1e094f2c3264', + 'merge': True, + 'message': 'Merge pull request #4693 from ' + 'Travmatth/fix-4252-BannerPlugin-placeholder\n' + '\n' + 'Fix 4252 banner plugin placeholder', + 'metadata': {}, + 'parents': ['53bb15b1ed64f8636036f773100d502909bd1e6b', + '08eca2fb4517bb2a14e21fa46e3291878c79be0e'], + 'synthetic': False, + 'type': 'git'}, + {'author': {'email': 'tobias.koppers@googlemail.com', + 'fullname': 'Tobias Koppers ', + 'id': 141959, + 'name': 'Tobias Koppers'}, + 'committer': {'email': 'tobias.koppers@googlemail.com', + 'fullname': 'Tobias Koppers ', + 'id': 141959, + 'name': 'Tobias Koppers'}, + 'committer_date': '2017-04-23T14:02:06+02:00', + 'date': '2017-04-23T12:50:01+02:00', + 'directory': '29cf488a00126c0e45c78b92f97cdbc733f4d07e', + 'id': '0f91f949e2d41ef5cb92493bcc6c1fa7578ac27d', + 'merge': False, + 'message': 'improve circleci build\n', + 'metadata': {}, + 'parents': ['53bb15b1ed64f8636036f773100d502909bd1e6b'], + 'synthetic': False, + 'type': 'git'}, + {'author': {'email': 'even.alander@videxio.com', + 'fullname': 'Even Alander ', + 'id': 3407016, + 'name': 'Even Alander'}, + 'committer': {'email': 'even.alander@videxio.com', + 'fullname': 'Even Alander ', + 'id': 3407016, + 'name': 'Even Alander'}, + 'committer_date': '2017-04-28T09:20:14+02:00', + 'date': '2017-04-28T09:20:14+02:00', + 'directory': 'f8b679bd62a33880d7f39f70233c3c14aead7c22', + 'id': '41310135bb4e30a0a6f71eddd3d74419ec8512a7', + 'merge': False, + 'message': 'rename stats.stackTrace to stats.moduleTrace\n', + 'metadata': {}, + 'parents': ['7e4310a1759bd35834290e4c4af207ce9b05ffd9'], + 'synthetic': False, + 'type': 'git'}, + {'author': {'email': 'tobias.koppers@googlemail.com', + 'fullname': 'Tobias Koppers ', + 'id': 141959, + 'name': 'Tobias Koppers'}, + 'committer': {'email': 'noreply@github.com', + 'fullname': 'GitHub ', + 'id': 10932771, + 'name': 'GitHub'}, + 'committer_date': '2017-04-23T12:10:20+02:00', + 'date': '2017-04-23T12:10:20+02:00', + 'directory': '1887ba14662a7e45567fa6daa39ab72975b429aa', + 'id': '53bb15b1ed64f8636036f773100d502909bd1e6b', + 'merge': True, + 'message': 'Merge pull request #3934 from ' + 'timse/refactor-watching-in-compiler\n' + '\n' + 'Refactor _done of Watching in compiler', + 'metadata': {}, + 'parents': ['b67d61abd6dd878f7a9aec4d10689c8b1678e28a', + 'ab30c6b1c0100634ad46bcaf47ff12963e22aac9'], + 'synthetic': False, + 'type': 'git'}, + {'author': {'email': 'tobias.koppers@googlemail.com', + 'fullname': 'Tobias Koppers ', + 'id': 141959, + 'name': 'Tobias Koppers'}, + 'committer': {'email': 'noreply@github.com', + 'fullname': 'GitHub ', + 'id': 10932771, + 'name': 'GitHub'}, + 'committer_date': '2017-04-23T12:00:53+02:00', + 'date': '2017-04-23T12:00:53+02:00', + 'directory': '4da5fda41a4b5cdbcf1fbb2604e9696c67106271', + 'id': '08eca2fb4517bb2a14e21fa46e3291878c79be0e', + 'merge': False, + 'message': 'spacing', + 'metadata': {}, + 'parents': ['7061c2c76b984e9c7ab893bf4b14b4b102b5fa0d'], + 'synthetic': False, + 'type': 'git'}, + {'author': {'email': 'even.alander@videxio.com', + 'fullname': 'Even Alander ', + 'id': 3407016, + 'name': 'Even Alander'}, + 'committer': {'email': 'even.alander@videxio.com', + 'fullname': 'Even Alander ', + 'id': 3407016, + 'name': 'Even Alander'}, + 'committer_date': '2017-04-27T10:22:20+02:00', + 'date': '2017-04-27T10:22:20+02:00', + 'directory': '21f3f99258fad8513cbdd40f549cb067362add27', + 'id': '7e4310a1759bd35834290e4c4af207ce9b05ffd9', + 'merge': False, + 'message': 'add option to lib/Stats.js to disable stack trace on errors and ' + 'warnings\n', + 'metadata': {}, + 'parents': ['24ef6ea1b56b358caeb4b07476a909f4f86c2d8a'], + 'synthetic': False, + 'type': 'git'}, + {'author': {'email': 'tobias.koppers@googlemail.com', + 'fullname': 'Tobias Koppers ', + 'id': 141959, + 'name': 'Tobias Koppers'}, + 'committer': {'email': 'noreply@github.com', + 'fullname': 'GitHub ', + 'id': 10932771, + 'name': 'GitHub'}, + 'committer_date': '2017-04-23T12:04:28+02:00', + 'date': '2017-04-23T12:04:28+02:00', + 'directory': 'effe11da8d44198a760cab867351f66d4b0da7d1', + 'id': 'b67d61abd6dd878f7a9aec4d10689c8b1678e28a', + 'merge': True, + 'message': 'Merge pull request #4755 from aretecode/lint-update\n' + '\n' + 'update node linting', + 'metadata': {}, + 'parents': ['a2ec4c8cea281715d3043a6dcb4198be039cfb54', + '5c423d6feb6521c9b5274ba342b7621c453f35eb'], + 'synthetic': False, + 'type': 'git'}, + {'author': {'email': 'tim.sebastian@gmail.com', + 'fullname': 'Tim Sebastian ', + 'id': 3654888, + 'name': 'Tim Sebastian'}, + 'committer': {'email': 'tim.sebastian@gmail.com', + 'fullname': 'Tim Sebastian ', + 'id': 3654888, + 'name': 'Tim Sebastian'}, + 'committer_date': '2017-04-08T09:13:38+10:00', + 'date': '2017-04-08T09:13:38+10:00', + 'directory': '20805e5ff5776062b9108138c4dfce8124185f5a', + 'id': 'ab30c6b1c0100634ad46bcaf47ff12963e22aac9', + 'merge': False, + 'message': 'pass stats to error handler again, to prevent breaking change\n', + 'metadata': {}, + 'parents': ['b863851ce969317688799b754131e3546206c7ad'], + 'synthetic': False, + 'type': 'git'}, + {'author': {'email': 'trav.matth@gmail.com', + 'fullname': 'Travis Matthews ', + 'id': 439443, + 'name': 'Travis Matthews'}, + 'committer': {'email': 'trav.matth@gmail.com', + 'fullname': 'Travis Matthews ', + 'id': 439443, + 'name': 'Travis Matthews'}, + 'committer_date': '2017-04-19T09:46:34-04:00', + 'date': '2017-04-19T09:46:34-04:00', + 'directory': '3c9ea2ac7fec857634b7b46dc08b52a5670106c5', + 'id': '7061c2c76b984e9c7ab893bf4b14b4b102b5fa0d', + 'merge': False, + 'message': 'added file test, remove unused [basename]\n', + 'metadata': {}, + 'parents': ['527f9434f74e63ae76ecbf18a4710589667dc731'], + 'synthetic': False, + 'type': 'git'}, + {'author': {'email': 'tobias.koppers@googlemail.com', + 'fullname': 'Tobias Koppers ', + 'id': 141959, + 'name': 'Tobias Koppers'}, + 'committer': {'email': 'noreply@github.com', + 'fullname': 'GitHub ', + 'id': 10932771, + 'name': 'GitHub'}, + 'committer_date': '2017-04-23T12:02:55+02:00', + 'date': '2017-04-23T12:02:55+02:00', + 'directory': '5185e53216f3a836598f946f7302155f5bb50d02', + 'id': 'a2ec4c8cea281715d3043a6dcb4198be039cfb54', + 'merge': True, + 'message': 'Merge pull request #4722 from ' + 'willmendesneto/refactor-format-location\n' + '\n' + 'refactor(formatLocation): upgrade to ES6', + 'metadata': {}, + 'parents': ['1c5f3bf59f8a1e06bbe83b7459362f69bb5b7460', + 'c826edde1be8af20a02166f792f79bbf2a231c4e'], + 'synthetic': False, + 'type': 'git'}, + {'author': {'email': 'tobias.koppers@googlemail.com', + 'fullname': 'Tobias Koppers ', + 'id': 141959, + 'name': 'Tobias Koppers'}, + 'committer': {'email': 'noreply@github.com', + 'fullname': 'GitHub ', + 'id': 10932771, + 'name': 'GitHub'}, + 'committer_date': '2017-04-22T19:02:01+02:00', + 'date': '2017-04-22T19:02:01+02:00', + 'directory': '461c61e52405649d272d7e79b8a1abe27e5b33b2', + 'id': '5c423d6feb6521c9b5274ba342b7621c453f35eb', + 'merge': False, + 'message': 'change supported node.js version to 4', + 'metadata': {}, + 'parents': ['219a95dddca951737d4b947fc2d59e3a2b5cbb23'], + 'synthetic': False, + 'type': 'git'}, + {'author': {'email': 'tim.sebastian@gmail.com', + 'fullname': 'Tim Sebastian ', + 'id': 3654888, + 'name': 'Tim Sebastian'}, + 'committer': {'email': 'tim.sebastian@gmail.com', + 'fullname': 'Tim Sebastian ', + 'id': 3654888, + 'name': 'Tim Sebastian'}, + 'committer_date': '2017-04-06T21:52:06+10:00', + 'date': '2017-04-06T21:52:06+10:00', + 'directory': '803b83492f4be6b472720dfd5dd0f45675213ac2', + 'id': 'b863851ce969317688799b754131e3546206c7ad', + 'merge': False, + 'message': 'add getStats again - remove breaking change\n', + 'metadata': {}, + 'parents': ['2a1bcff9e6999d471747ab3575d82c33a0e5e082'], + 'synthetic': False, + 'type': 'git'}, + {'author': {'email': 'trav.matth@gmail.com', + 'fullname': 'Travis Matthews ', + 'id': 439443, + 'name': 'Travis Matthews'}, + 'committer': {'email': 'trav.matth@gmail.com', + 'fullname': 'Travis Matthews ', + 'id': 439443, + 'name': 'Travis Matthews'}, + 'committer_date': '2017-04-19T09:28:25-04:00', + 'date': '2017-04-19T09:28:25-04:00', + 'directory': 'aafcbc664c51bf17dcfde6b7112efe95331c6fcd', + 'id': '527f9434f74e63ae76ecbf18a4710589667dc731', + 'merge': False, + 'message': 'removed single quotes\n', + 'metadata': {}, + 'parents': ['c68bd16b265b6d5f8478e5728ea077ead90e8a37'], + 'synthetic': False, + 'type': 'git'}, + {'author': {'email': 'tobias.koppers@googlemail.com', + 'fullname': 'Tobias Koppers ', + 'id': 141959, + 'name': 'Tobias Koppers'}, + 'committer': {'email': 'noreply@github.com', + 'fullname': 'GitHub ', + 'id': 10932771, + 'name': 'GitHub'}, + 'committer_date': '2017-04-23T11:49:28+02:00', + 'date': '2017-04-23T11:49:28+02:00', + 'directory': 'cf61c993ed0335a88d7975aba07ef743eaa7b845', + 'id': '1c5f3bf59f8a1e06bbe83b7459362f69bb5b7460', + 'merge': True, + 'message': 'Merge pull request #4748 from ' + 'aretecode/examples-dll-readme-update2\n' + '\n' + 'examples/dll readme updates', + 'metadata': {}, + 'parents': ['cbd493904f222abac1d33918b4686b0bc4de3a42', + 'f223aecd05b47b9cfff34609f730c81d03034514'], + 'synthetic': False, + 'type': 'git'}, + {'author': {'email': 'willmendesneto@gmail.com', + 'fullname': 'Will Mendes ', + 'id': 93385, + 'name': 'Will Mendes'}, + 'committer': {'email': 'willmendesneto@gmail.com', + 'fullname': 'Will Mendes ', + 'id': 93385, + 'name': 'Will Mendes'}, + 'committer_date': '2017-04-17T22:00:47+10:00', + 'date': '2017-04-17T22:00:47+10:00', + 'directory': '817b5de0d36aae8f3243b472e19b2fae1adf9173', + 'id': 'c826edde1be8af20a02166f792f79bbf2a231c4e', + 'merge': False, + 'message': 'refactor(formatLocation): upgrade to ES6\n', + 'metadata': {}, + 'parents': ['82ddd16080663e3773bdac2a62082991acb8637e'], + 'synthetic': False, + 'type': 'git'}, + {'author': {'email': 'aretecode@gmail.com', + 'fullname': 'James ', + 'id': 11020868, + 'name': 'James'}, + 'committer': {'email': 'noreply@github.com', + 'fullname': 'GitHub ', + 'id': 10932771, + 'name': 'GitHub'}, + 'committer_date': '2017-04-19T23:53:44-07:00', + 'date': '2017-04-19T23:53:44-07:00', + 'directory': 'fa17b78edd5fd19a589663489e91b14e2c6337c0', + 'id': '219a95dddca951737d4b947fc2d59e3a2b5cbb23', + 'merge': False, + 'message': 'lint autofix the eslint file', + 'metadata': {}, + 'parents': ['cf666188298dfe75cc343018bf35b1c753fb6636'], + 'synthetic': False, + 'type': 'git'}, + {'author': {'email': 'tim.sebastian@gmail.com', + 'fullname': 'Tim Sebastian ', + 'id': 3654888, + 'name': 'Tim Sebastian'}, + 'committer': {'email': 'tim.sebastian@gmail.com', + 'fullname': 'Tim Sebastian ', + 'id': 3654888, + 'name': 'Tim Sebastian'}, + 'committer_date': '2017-04-06T21:46:07+10:00', + 'date': '2017-01-12T22:33:03+11:00', + 'directory': '7c6b85c086b56f11cf29bc3d13836145dc225407', + 'id': '2a1bcff9e6999d471747ab3575d82c33a0e5e082', + 'merge': False, + 'message': 'refactor the _done method of Watching\n' + '\n' + '- we can safely ignore the "else" cases of not having an error ' + 'as _done() is only called without arguments if "this.invalid is ' + 'true"\n' + '- if we get passed the point of `this.invalid` either `err` or ' + '`compilation` are !!always!! set. therefore later checks can ' + 'again be ignored\n' + '- early return in error case\n' + '- ignore `this.error` if we make it passed the error as it will ' + 'be unset at this point.\n' + '- remove the setting of `this.error` or `this.stats` as the only ' + 'use is inside this method and only allow weird behaviour if ' + 'someone set them from outside\n', + 'metadata': {}, + 'parents': ['8165164d3e50fef3252d7c274cef1c0b595c6992'], + 'synthetic': False, + 'type': 'git'}, + {'author': {'email': 'trav.matth@gmail.com', + 'fullname': 'Travis Matthews ', + 'id': 439443, + 'name': 'Travis Matthews'}, + 'committer': {'email': 'trav.matth@gmail.com', + 'fullname': 'Travis Matthews ', + 'id': 439443, + 'name': 'Travis Matthews'}, + 'committer_date': '2017-04-19T08:41:00-04:00', + 'date': '2017-04-19T08:41:00-04:00', + 'directory': '34bfecaa341626e1c60d1d2a1f697fb1f74f8bcd', + 'id': 'c68bd16b265b6d5f8478e5728ea077ead90e8a37', + 'merge': False, + 'message': 'remove comment\n', + 'metadata': {}, + 'parents': ['f2e30693ac0e3a9a0fe6a01a89fe6072dee7f405'], + 'synthetic': False, + 'type': 'git'}, + {'author': {'email': 'tobias.koppers@googlemail.com', + 'fullname': 'Tobias Koppers ', + 'id': 141959, + 'name': 'Tobias Koppers'}, + 'committer': {'email': 'noreply@github.com', + 'fullname': 'GitHub ', + 'id': 10932771, + 'name': 'GitHub'}, + 'committer_date': '2017-04-22T12:45:04+02:00', + 'date': '2017-04-22T12:45:04+02:00', + 'directory': '412c6f5c00ce7d02efcdd6f23160a36eb4e5a1f4', + 'id': 'cbd493904f222abac1d33918b4686b0bc4de3a42', + 'merge': True, + 'message': 'Merge pull request #4768 from xizhao/patch-1\n' + '\n' + 'Add license scan report and status', + 'metadata': {}, + 'parents': ['d7f30392ddda27613e0ea05cb60ec985b4f75e5c', + 'ff211108d888908d41470cea6187133ccdb56e87'], + 'synthetic': False, + 'type': 'git'}, + {'author': {'email': 'aretecode@gmail.com', + 'fullname': 'Arete Code ', + 'id': 8227899, + 'name': 'Arete Code'}, + 'committer': {'email': 'aretecode@gmail.com', + 'fullname': 'Arete Code ', + 'id': 8227899, + 'name': 'Arete Code'}, + 'committer_date': '2017-04-21T15:48:06-07:00', + 'date': '2017-04-21T15:48:06-07:00', + 'directory': '9d69f66cf3d2fa5119910bfd6a16f16e996795cc', + 'id': 'f223aecd05b47b9cfff34609f730c81d03034514', + 'merge': False, + 'message': 'rebase for semicolon\n', + 'metadata': {}, + 'parents': ['914fe2c923533d43f5edff54b1704bbd3dd407bc'], + 'synthetic': False, + 'type': 'git'}, + {'author': {'email': 'tobias.koppers@googlemail.com', + 'fullname': 'Tobias Koppers ', + 'id': 141959, + 'name': 'Tobias Koppers'}, + 'committer': {'email': 'noreply@github.com', + 'fullname': 'GitHub ', + 'id': 10932771, + 'name': 'GitHub'}, + 'committer_date': '2017-04-15T19:26:54+02:00', + 'date': '2017-04-15T19:26:54+02:00', + 'directory': 'c2da553b359190d3dc4266d77e311bd002d7ccd2', + 'id': '82ddd16080663e3773bdac2a62082991acb8637e', + 'merge': True, + 'message': 'Merge pull request #4717 from STRML/fix/hashSalt\n' + '\n' + 'fix missing `hashSalt` from options schema', + 'metadata': {}, + 'parents': ['bd753567da1248624beaaea14af31d6dbe303411', + '805c9fadf05c00f3df16d1faf4d3f608b78e3b59'], + 'synthetic': False, + 'type': 'git'}, + {'author': {'email': 'aretecode@gmail.com', + 'fullname': 'Arete Code ', + 'id': 8227899, + 'name': 'Arete Code'}, + 'committer': {'email': 'aretecode@gmail.com', + 'fullname': 'Arete Code ', + 'id': 8227899, + 'name': 'Arete Code'}, + 'committer_date': '2017-04-19T19:05:28-07:00', + 'date': '2017-04-19T19:05:28-07:00', + 'directory': 'dd7dc17a8a28b6bb415b1f6980de6fcd3715847c', + 'id': 'cf666188298dfe75cc343018bf35b1c753fb6636', + 'merge': False, + 'message': '👕 update node linting\n' + '\n' + 'update linting for destructuring and latest node\n' + '\n' + 'uses .js rather than .eslintrc file\n' + '\n' + 'pulls in rules from\n' + 'https://github.com/webpack/webpack-cli/pull/46/files#diff-df39304d828831c44a2b9f38cd45289cR40\n' + '\n' + 'adds spacing for this\n' + 'screen shot 2017-04-19 at 7 03 16 pm\n', + 'metadata': {}, + 'parents': ['8aa8a7b63fed3c7909a6d4f15159c036a0561d64'], + 'synthetic': False, + 'type': 'git'} +] \ No newline at end of file diff --git a/swh/web/tests/browse/views/test_revision.py b/swh/web/tests/browse/views/test_revision.py new file mode 100644 index 00000000..749ec918 --- /dev/null +++ b/swh/web/tests/browse/views/test_revision.py @@ -0,0 +1,159 @@ +# Copyright (C) 2017 The Software Heritage developers +# See the AUTHORS file at the top-level directory of this distribution +# License: GNU General Public License version 3, or any later version +# See top-level LICENSE file for more information + +from unittest.mock import patch +from nose.tools import istest +from django.test import TestCase +from django.utils.html import escape + +from swh.web.common.utils import reverse, format_utc_iso_date + +from .data.revision_test_data import ( + revision_id_test, revision_metadata_test, + revision_history_log_test +) + + +class SwhBrowseRevisionTest(TestCase): + + @patch('swh.web.browse.views.revision.service') + @istest + def test_revision_browse(self, mock_service): + mock_service.lookup_revision.return_value = revision_metadata_test + + url = reverse('browse-revision', + kwargs={'sha1_git': revision_id_test}) + + author_id = revision_metadata_test['author']['id'] + author_name = revision_metadata_test['author']['name'] + committer_id = revision_metadata_test['committer']['id'] + committer_name = revision_metadata_test['committer']['name'] + dir_id = revision_metadata_test['directory'] + + author_url = reverse('browse-person', + kwargs={'person_id': author_id}) + committer_url = reverse('browse-person', + kwargs={'person_id': committer_id}) + + directory_url = reverse('browse-directory', + kwargs={'sha1_git': dir_id}) + + history_url = reverse('browse-revision-log', + kwargs={'sha1_git': revision_id_test}) + + resp = self.client.get(url) + + self.assertEquals(resp.status_code, 200) + self.assertTemplateUsed('revision.html') + self.assertContains(resp, '%s' % + (author_url, author_name)) + self.assertContains(resp, '%s' % + (committer_url, committer_name)) + self.assertContains(resp, '%s' % + (directory_url, dir_id)) + self.assertContains(resp, '%s' % + (history_url, history_url)) + + for parent in revision_metadata_test['parents']: + parent_url = reverse('browse-revision', + kwargs={'sha1_git': parent}) + self.assertContains(resp, '%s' % + (parent_url, parent)) + + author_date = revision_metadata_test['date'] + committer_date = revision_metadata_test['committer_date'] + message = revision_metadata_test['message'] + + self.assertContains(resp, format_utc_iso_date(author_date)) + self.assertContains(resp, format_utc_iso_date(committer_date)) + self.assertContains(resp, message) + + @patch('swh.web.browse.views.revision.service') + @istest + def test_revision_log_browse(self, mock_service): + per_page = 10 + mock_service.lookup_revision_log.return_value = \ + revision_history_log_test[:per_page+1] + + url = reverse('browse-revision-log', + kwargs={'sha1_git': revision_id_test}, + query_params={'per_page': per_page}) + + resp = self.client.get(url) + + prev_rev = revision_history_log_test[per_page]['id'] + next_page_url = reverse('browse-revision-log', + kwargs={'sha1_git': prev_rev}, + query_params={'revs_breadcrumb': revision_id_test, # noqa + 'per_page': per_page}) + + self.assertEquals(resp.status_code, 200) + self.assertTemplateUsed('revision-log.html') + self.assertContains(resp, '', + count=per_page) + self.assertContains(resp, '
  • Newer
  • ') + self.assertContains(resp, '
  • Older
  • ' % + escape(next_page_url)) + + for log in revision_history_log_test[:per_page]: + author_url = reverse('browse-person', + kwargs={'person_id': log['author']['id']}) + revision_url = reverse('browse-revision', + kwargs={'sha1_git': log['id']}) + directory_url = reverse('browse-directory', + kwargs={'sha1_git': log['directory']}) + self.assertContains(resp, '%s' % + (author_url, log['author']['name'])) + self.assertContains(resp, '%s' % + (revision_url, log['id'][:7])) + self.assertContains(resp, '%s' % + (directory_url, 'Tree')) + + mock_service.lookup_revision_log.return_value = \ + revision_history_log_test[per_page:2*per_page+1] + + resp = self.client.get(next_page_url) + + prev_prev_rev = revision_history_log_test[2*per_page]['id'] + prev_page_url = reverse('browse-revision-log', + kwargs={'sha1_git': revision_id_test}, + query_params={'per_page': per_page}) + next_page_url = reverse('browse-revision-log', + kwargs={'sha1_git': prev_prev_rev}, + query_params={'revs_breadcrumb': revision_id_test + '/' + prev_rev, # noqa + 'per_page': per_page}) + + self.assertEquals(resp.status_code, 200) + self.assertTemplateUsed('revision-log.html') + self.assertContains(resp, '', + count=per_page) + self.assertContains(resp, '
  • Newer
  • ' % + escape(prev_page_url)) + self.assertContains(resp, '
  • Older
  • ' % + escape(next_page_url)) + + mock_service.lookup_revision_log.return_value = \ + revision_history_log_test[2*per_page:3*per_page+1] + + resp = self.client.get(next_page_url) + + prev_prev_prev_rev = revision_history_log_test[3*per_page]['id'] + prev_page_url = reverse('browse-revision-log', + kwargs={'sha1_git': prev_rev}, + query_params={'revs_breadcrumb': revision_id_test, # noqa + 'per_page': per_page}) + next_page_url = reverse('browse-revision-log', + kwargs={'sha1_git': prev_prev_prev_rev}, + query_params={'revs_breadcrumb': revision_id_test + '/' + prev_rev + '/' + prev_prev_rev, # noqa + 'per_page': per_page}) + + self.assertEquals(resp.status_code, 200) + self.assertTemplateUsed('revision-log.html') + self.assertContains(resp, '', + count=per_page) + self.assertContains(resp, '
  • Newer
  • ' % + escape(prev_page_url)) + self.assertContains(resp, '
  • Older
  • ' % + escape(next_page_url)) diff --git a/swh/web/tests/common/test_utils.py b/swh/web/tests/common/test_utils.py index 5d7a645c..d3459bcd 100644 --- a/swh/web/tests/common/test_utils.py +++ b/swh/web/tests/common/test_utils.py @@ -1,87 +1,92 @@ # Copyright (C) 2017 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information import datetime import dateutil import unittest from nose.tools import istest from swh.web.common import utils class UtilsTestCase(unittest.TestCase): @istest def shorten_path_noop(self): noops = [ '/api/', '/browse/', '/content/symbol/foobar/' ] for noop in noops: self.assertEqual( utils.shorten_path(noop), noop ) @istest def shorten_path_sha1(self): sha1 = 'aafb16d69fd30ff58afdd69036a26047f3aebdc6' short_sha1 = sha1[:8] + '...' templates = [ '/api/1/content/sha1:%s/', '/api/1/content/sha1_git:%s/', '/api/1/directory/%s/', '/api/1/content/sha1:%s/ctags/', ] for template in templates: self.assertEqual( utils.shorten_path(template % sha1), template % short_sha1 ) @istest def shorten_path_sha256(self): sha256 = ('aafb16d69fd30ff58afdd69036a26047' '213add102934013a014dfca031c41aef') short_sha256 = sha256[:8] + '...' templates = [ '/api/1/content/sha256:%s/', '/api/1/directory/%s/', '/api/1/content/sha256:%s/filetype/', ] for template in templates: self.assertEqual( utils.shorten_path(template % sha256), template % short_sha256 ) @istest def parse_timestamp(self): input_timestamps = [ None, '2016-01-12', '2016-01-12T09:19:12+0100', 'Today is January 1, 2047 at 8:21:00AM', '1452591542', ] output_dates = [ None, datetime.datetime(2016, 1, 12, 0, 0), datetime.datetime(2016, 1, 12, 9, 19, 12, tzinfo=dateutil.tz.tzoffset(None, 3600)), datetime.datetime(2047, 1, 1, 8, 21), datetime.datetime(2016, 1, 12, 9, 39, 2, tzinfo=datetime.timezone.utc), ] for ts, exp_date in zip(input_timestamps, output_dates): self.assertEquals(utils.parse_timestamp(ts), exp_date) + + @istest + def format_utc_iso_date(self): + self.assertEqual(utils.format_utc_iso_date('2017-05-04T13:27:13+02:00'), # noqa + '04 May 2017, 13:27 UTC')