{"action":"created","comment":{"url":"https://api.github.com/repos/dimagi/commcare-hq/pulls/comments/40288156","id":40288156,"diff_hunk":"@@ -250,3 +257,100 @@ def update_build_comment(request, domain, app_id):\n     build.build_comment = request.POST.get('comment')\n     build.save()\n     return json_response({'status': 'success'})\n+\n+\n+def _get_app_diff_files(app):\n+    \"\"\"\n+    Return a dict of the files that an app build is comprised of. Return dict\n+    also includes the app json.\n+    \"\"\"\n+    files = dict(download_index_files(app))\n+    files[\"app.json\"] = json.dumps(\n+        app.to_json(), sort_keys=True, indent=4, separators=(',', ': ')\n+    )\n+    return files\n+\n+\n+def _get_change_counts(html_diff):\n+    diff_lines = html_diff.splitlines()\n+    additions = sum(1 for line in diff_lines if line.startswith('<div class=\"insert\">'))\n+    deletions = sum(1 for line in diff_lines if line.startswith('<div class=\"delete\">'))\n+    return additions, deletions\n+\n+\n+def _get_file_pairs(first_app, second_app):\n+    \"\"\"\n+    :param first_app:\n+    :param second_app:\n+    :return: A dictionary mapping file name to tuple where the first element is\n+     the corresponding file on the first app and the second is the corresponding\n+     file on the second app.\n+     \"files\" will be empty strings if the file does not exist on the app.\n+    \"\"\"\n+    first_app_files = _get_app_diff_files(first_app)\n+    second_app_files = _get_app_diff_files(second_app)\n+    file_names = set(first_app_files.keys()) | set(second_app_files.keys())\n+    file_pairs = {\n+        n: (first_app_files.get(n, \"\"), second_app_files.get(n, \"\"))\n+        for n in file_names\n+    }\n+    return file_pairs\n+\n+\n+def _get_app_diffs(first_app, second_app):\n+    \"\"\"\n+    Return a list of tuples. The first value in each tuple is a file name,\n+    the second value is an html snippet representing the diff of that file\n+    in the two given apps.\n+    \"\"\"\n+    file_pairs = _get_file_pairs(first_app, second_app)\n+    diffs = []\n+    for name, files in file_pairs.iteritems():\n+        diff_html = ghdiff.diff(files[0], files[1], n=4, css=False)\n+        additions, deletions = _get_change_counts(diff_html)\n+        if additions == 0 and deletions == 0:\n+            diff_html = \"\"\n+        diffs.append((name, diff_html, additions, deletions))\n+    return sorted(diffs)","path":"corehq/apps/app_manager/views/releases.py","position":88,"original_position":88,"commit_id":"fef53f0ac5fcc2d888df1de3b4e03458003a49e4","original_commit_id":"fef53f0ac5fcc2d888df1de3b4e03458003a49e4","user":{"login":"kaapstorm","id":708421,"avatar_url":"https://avatars.githubusercontent.com/u/708421?v=3","gravatar_id":"","url":"https://api.github.com/users/kaapstorm","html_url":"https://github.com/kaapstorm","followers_url":"https://api.github.com/users/kaapstorm/followers","following_url":"https://api.github.com/users/kaapstorm/following{/other_user}","gists_url":"https://api.github.com/users/kaapstorm/gists{/gist_id}","starred_url":"https://api.github.com/users/kaapstorm/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/kaapstorm/subscriptions","organizations_url":"https://api.github.com/users/kaapstorm/orgs","repos_url":"https://api.github.com/users/kaapstorm/repos","events_url":"https://api.github.com/users/kaapstorm/events{/privacy}","received_events_url":"https://api.github.com/users/kaapstorm/received_events","type":"User","site_admin":false},"body":"I second @esoergel s comment, and I like that not only are they small and self-explanatory, but also functional. If it wasn't for the fact that they need two versions of an actual app, which would need some work before you can mock in one line, they'd be great candidates for a short doctest each.","created_at":"2015-09-24T06:05:53Z","updated_at":"2015-09-24T06:05:53Z","html_url":"https://github.com/dimagi/commcare-hq/pull/8290#discussion_r40288156","pull_request_url":"https://api.github.com/repos/dimagi/commcare-hq/pulls/8290","_links":{"self":{"href":"https://api.github.com/repos/dimagi/commcare-hq/pulls/comments/40288156"},"html":{"href":"https://github.com/dimagi/commcare-hq/pull/8290#discussion_r40288156"},"pull_request":{"href":"https://api.github.com/repos/dimagi/commcare-hq/pulls/8290"}}},"pull_request":{"url":"https://api.github.com/repos/dimagi/commcare-hq/pulls/8290","id":45620597,"html_url":"https://github.com/dimagi/commcare-hq/pull/8290","diff_url":"https://github.com/dimagi/commcare-hq/pull/8290.diff","patch_url":"https://github.com/dimagi/commcare-hq/pull/8290.patch","issue_url":"https://api.github.com/repos/dimagi/commcare-hq/issues/8290","number":8290,"state":"open","locked":false,"title":"Add app \"diff\" page (compare two app builds)","user":{"login":"NoahCarnahan","id":2117127,"avatar_url":"https://avatars.githubusercontent.com/u/2117127?v=3","gravatar_id":"","url":"https://api.github.com/users/NoahCarnahan","html_url":"https://github.com/NoahCarnahan","followers_url":"https://api.github.com/users/NoahCarnahan/followers","following_url":"https://api.github.com/users/NoahCarnahan/following{/other_user}","gists_url":"https://api.github.com/users/NoahCarnahan/gists{/gist_id}","starred_url":"https://api.github.com/users/NoahCarnahan/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/NoahCarnahan/subscriptions","organizations_url":"https://api.github.com/users/NoahCarnahan/orgs","repos_url":"https://api.github.com/users/NoahCarnahan/repos","events_url":"https://api.github.com/users/NoahCarnahan/events{/privacy}","received_events_url":"https://api.github.com/users/NoahCarnahan/received_events","type":"User","site_admin":false},"body":"@dimagi/team-commcare-hq: a little page I whipped up (mostly) during the hackathon.\r\n\r\nAdds a page to hq at `/a/<domain>/apps/diff/<build_id_1>/<build_id_2>/` to compare two builds of an app. The hope is that this page can be used to help debug broken builds and keep track of changes in large apps. The page displays diffs of all the relevant files (such as form xml, suite files, and app json). At the moment this is a \"hidden\" feature. There are no links to this page within hq. But, @orangejenny is working on adding a \"compare\" button to the releases page that will link to this page (and perhaps sometime in the future will include a non-technical app summary diff).\r\n\r\nExample:\r\n<img width=\"1425\" alt=\"screen shot 2015-09-21 at 9 38 35 pm\" src=\"https://cloud.githubusercontent.com/assets/2117127/10009474/81c2f9e2-60ab-11e5-9d10-1f2934e39c84.png\">\r\n","created_at":"2015-09-22T01:56:19Z","updated_at":"2015-09-24T06:05:53Z","closed_at":null,"merged_at":null,"merge_commit_sha":"78de2d4823b08d0aaf5271040c2dfba8485cbde7","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/dimagi/commcare-hq/pulls/8290/commits","review_comments_url":"https://api.github.com/repos/dimagi/commcare-hq/pulls/8290/comments","review_comment_url":"https://api.github.com/repos/dimagi/commcare-hq/pulls/comments{/number}","comments_url":"https://api.github.com/repos/dimagi/commcare-hq/issues/8290/comments","statuses_url":"https://api.github.com/repos/dimagi/commcare-hq/statuses/fef53f0ac5fcc2d888df1de3b4e03458003a49e4","head":{"label":"dimagi:app-diff","ref":"app-diff","sha":"fef53f0ac5fcc2d888df1de3b4e03458003a49e4","user":{"login":"dimagi","id":94514,"avatar_url":"https://avatars.githubusercontent.com/u/94514?v=3","gravatar_id":"","url":"https://api.github.com/users/dimagi","html_url":"https://github.com/dimagi","followers_url":"https://api.github.com/users/dimagi/followers","following_url":"https://api.github.com/users/dimagi/following{/other_user}","gists_url":"https://api.github.com/users/dimagi/gists{/gist_id}","starred_url":"https://api.github.com/users/dimagi/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dimagi/subscriptions","organizations_url":"https://api.github.com/users/dimagi/orgs","repos_url":"https://api.github.com/users/dimagi/repos","events_url":"https://api.github.com/users/dimagi/events{/privacy}","received_events_url":"https://api.github.com/users/dimagi/received_events","type":"Organization","site_admin":false},"repo":{"id":247278,"name":"commcare-hq","full_name":"dimagi/commcare-hq","owner":{"login":"dimagi","id":94514,"avatar_url":"https://avatars.githubusercontent.com/u/94514?v=3","gravatar_id":"","url":"https://api.github.com/users/dimagi","html_url":"https://github.com/dimagi","followers_url":"https://api.github.com/users/dimagi/followers","following_url":"https://api.github.com/users/dimagi/following{/other_user}","gists_url":"https://api.github.com/users/dimagi/gists{/gist_id}","starred_url":"https://api.github.com/users/dimagi/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dimagi/subscriptions","organizations_url":"https://api.github.com/users/dimagi/orgs","repos_url":"https://api.github.com/users/dimagi/repos","events_url":"https://api.github.com/users/dimagi/events{/privacy}","received_events_url":"https://api.github.com/users/dimagi/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/dimagi/commcare-hq","description":"A server-side tool to help manage community health workers, written in Django","fork":false,"url":"https://api.github.com/repos/dimagi/commcare-hq","forks_url":"https://api.github.com/repos/dimagi/commcare-hq/forks","keys_url":"https://api.github.com/repos/dimagi/commcare-hq/keys{/key_id}","collaborators_url":"https://api.github.com/repos/dimagi/commcare-hq/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/dimagi/commcare-hq/teams","hooks_url":"https://api.github.com/repos/dimagi/commcare-hq/hooks","issue_events_url":"https://api.github.com/repos/dimagi/commcare-hq/issues/events{/number}","events_url":"https://api.github.com/repos/dimagi/commcare-hq/events","assignees_url":"https://api.github.com/repos/dimagi/commcare-hq/assignees{/user}","branches_url":"https://api.github.com/repos/dimagi/commcare-hq/branches{/branch}","tags_url":"https://api.github.com/repos/dimagi/commcare-hq/tags","blobs_url":"https://api.github.com/repos/dimagi/commcare-hq/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/dimagi/commcare-hq/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/dimagi/commcare-hq/git/refs{/sha}","trees_url":"https://api.github.com/repos/dimagi/commcare-hq/git/trees{/sha}","statuses_url":"https://api.github.com/repos/dimagi/commcare-hq/statuses/{sha}","languages_url":"https://api.github.com/repos/dimagi/commcare-hq/languages","stargazers_url":"https://api.github.com/repos/dimagi/commcare-hq/stargazers","contributors_url":"https://api.github.com/repos/dimagi/commcare-hq/contributors","subscribers_url":"https://api.github.com/repos/dimagi/commcare-hq/subscribers","subscription_url":"https://api.github.com/repos/dimagi/commcare-hq/subscription","commits_url":"https://api.github.com/repos/dimagi/commcare-hq/commits{/sha}","git_commits_url":"https://api.github.com/repos/dimagi/commcare-hq/git/commits{/sha}","comments_url":"https://api.github.com/repos/dimagi/commcare-hq/comments{/number}","issue_comment_url":"https://api.github.com/repos/dimagi/commcare-hq/issues/comments{/number}","contents_url":"https://api.github.com/repos/dimagi/commcare-hq/contents/{+path}","compare_url":"https://api.github.com/repos/dimagi/commcare-hq/compare/{base}...{head}","merges_url":"https://api.github.com/repos/dimagi/commcare-hq/merges","archive_url":"https://api.github.com/repos/dimagi/commcare-hq/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/dimagi/commcare-hq/downloads","issues_url":"https://api.github.com/repos/dimagi/commcare-hq/issues{/number}","pulls_url":"https://api.github.com/repos/dimagi/commcare-hq/pulls{/number}","milestones_url":"https://api.github.com/repos/dimagi/commcare-hq/milestones{/number}","notifications_url":"https://api.github.com/repos/dimagi/commcare-hq/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/dimagi/commcare-hq/labels{/name}","releases_url":"https://api.github.com/repos/dimagi/commcare-hq/releases{/id}","created_at":"2009-07-09T17:00:07Z","updated_at":"2015-09-03T14:30:33Z","pushed_at":"2015-09-24T05:29:01Z","git_url":"git://github.com/dimagi/commcare-hq.git","ssh_url":"git@github.com:dimagi/commcare-hq.git","clone_url":"https://github.com/dimagi/commcare-hq.git","svn_url":"https://github.com/dimagi/commcare-hq","homepage":"http://www.commcarehq.org","size":414104,"stargazers_count":50,"watchers_count":50,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":true,"forks_count":61,"mirror_url":null,"open_issues_count":67,"forks":61,"open_issues":67,"watchers":50,"default_branch":"master"}},"base":{"label":"dimagi:master","ref":"master","sha":"ddb957525008b9ece45073ec9bca3c0a64e57162","user":{"login":"dimagi","id":94514,"avatar_url":"https://avatars.githubusercontent.com/u/94514?v=3","gravatar_id":"","url":"https://api.github.com/users/dimagi","html_url":"https://github.com/dimagi","followers_url":"https://api.github.com/users/dimagi/followers","following_url":"https://api.github.com/users/dimagi/following{/other_user}","gists_url":"https://api.github.com/users/dimagi/gists{/gist_id}","starred_url":"https://api.github.com/users/dimagi/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dimagi/subscriptions","organizations_url":"https://api.github.com/users/dimagi/orgs","repos_url":"https://api.github.com/users/dimagi/repos","events_url":"https://api.github.com/users/dimagi/events{/privacy}","received_events_url":"https://api.github.com/users/dimagi/received_events","type":"Organization","site_admin":false},"repo":{"id":247278,"name":"commcare-hq","full_name":"dimagi/commcare-hq","owner":{"login":"dimagi","id":94514,"avatar_url":"https://avatars.githubusercontent.com/u/94514?v=3","gravatar_id":"","url":"https://api.github.com/users/dimagi","html_url":"https://github.com/dimagi","followers_url":"https://api.github.com/users/dimagi/followers","following_url":"https://api.github.com/users/dimagi/following{/other_user}","gists_url":"https://api.github.com/users/dimagi/gists{/gist_id}","starred_url":"https://api.github.com/users/dimagi/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dimagi/subscriptions","organizations_url":"https://api.github.com/users/dimagi/orgs","repos_url":"https://api.github.com/users/dimagi/repos","events_url":"https://api.github.com/users/dimagi/events{/privacy}","received_events_url":"https://api.github.com/users/dimagi/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/dimagi/commcare-hq","description":"A server-side tool to help manage community health workers, written in Django","fork":false,"url":"https://api.github.com/repos/dimagi/commcare-hq","forks_url":"https://api.github.com/repos/dimagi/commcare-hq/forks","keys_url":"https://api.github.com/repos/dimagi/commcare-hq/keys{/key_id}","collaborators_url":"https://api.github.com/repos/dimagi/commcare-hq/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/dimagi/commcare-hq/teams","hooks_url":"https://api.github.com/repos/dimagi/commcare-hq/hooks","issue_events_url":"https://api.github.com/repos/dimagi/commcare-hq/issues/events{/number}","events_url":"https://api.github.com/repos/dimagi/commcare-hq/events","assignees_url":"https://api.github.com/repos/dimagi/commcare-hq/assignees{/user}","branches_url":"https://api.github.com/repos/dimagi/commcare-hq/branches{/branch}","tags_url":"https://api.github.com/repos/dimagi/commcare-hq/tags","blobs_url":"https://api.github.com/repos/dimagi/commcare-hq/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/dimagi/commcare-hq/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/dimagi/commcare-hq/git/refs{/sha}","trees_url":"https://api.github.com/repos/dimagi/commcare-hq/git/trees{/sha}","statuses_url":"https://api.github.com/repos/dimagi/commcare-hq/statuses/{sha}","languages_url":"https://api.github.com/repos/dimagi/commcare-hq/languages","stargazers_url":"https://api.github.com/repos/dimagi/commcare-hq/stargazers","contributors_url":"https://api.github.com/repos/dimagi/commcare-hq/contributors","subscribers_url":"https://api.github.com/repos/dimagi/commcare-hq/subscribers","subscription_url":"https://api.github.com/repos/dimagi/commcare-hq/subscription","commits_url":"https://api.github.com/repos/dimagi/commcare-hq/commits{/sha}","git_commits_url":"https://api.github.com/repos/dimagi/commcare-hq/git/commits{/sha}","comments_url":"https://api.github.com/repos/dimagi/commcare-hq/comments{/number}","issue_comment_url":"https://api.github.com/repos/dimagi/commcare-hq/issues/comments{/number}","contents_url":"https://api.github.com/repos/dimagi/commcare-hq/contents/{+path}","compare_url":"https://api.github.com/repos/dimagi/commcare-hq/compare/{base}...{head}","merges_url":"https://api.github.com/repos/dimagi/commcare-hq/merges","archive_url":"https://api.github.com/repos/dimagi/commcare-hq/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/dimagi/commcare-hq/downloads","issues_url":"https://api.github.com/repos/dimagi/commcare-hq/issues{/number}","pulls_url":"https://api.github.com/repos/dimagi/commcare-hq/pulls{/number}","milestones_url":"https://api.github.com/repos/dimagi/commcare-hq/milestones{/number}","notifications_url":"https://api.github.com/repos/dimagi/commcare-hq/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/dimagi/commcare-hq/labels{/name}","releases_url":"https://api.github.com/repos/dimagi/commcare-hq/releases{/id}","created_at":"2009-07-09T17:00:07Z","updated_at":"2015-09-03T14:30:33Z","pushed_at":"2015-09-24T05:29:01Z","git_url":"git://github.com/dimagi/commcare-hq.git","ssh_url":"git@github.com:dimagi/commcare-hq.git","clone_url":"https://github.com/dimagi/commcare-hq.git","svn_url":"https://github.com/dimagi/commcare-hq","homepage":"http://www.commcarehq.org","size":414104,"stargazers_count":50,"watchers_count":50,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":true,"forks_count":61,"mirror_url":null,"open_issues_count":67,"forks":61,"open_issues":67,"watchers":50,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/dimagi/commcare-hq/pulls/8290"},"html":{"href":"https://github.com/dimagi/commcare-hq/pull/8290"},"issue":{"href":"https://api.github.com/repos/dimagi/commcare-hq/issues/8290"},"comments":{"href":"https://api.github.com/repos/dimagi/commcare-hq/issues/8290/comments"},"review_comments":{"href":"https://api.github.com/repos/dimagi/commcare-hq/pulls/8290/comments"},"review_comment":{"href":"https://api.github.com/repos/dimagi/commcare-hq/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/dimagi/commcare-hq/pulls/8290/commits"},"statuses":{"href":"https://api.github.com/repos/dimagi/commcare-hq/statuses/fef53f0ac5fcc2d888df1de3b4e03458003a49e4"}}}}