1 /**
2 	Copyright: © 2013 rejectedsoftware e.K.
3 	License: Subject to the terms of the GNU GPLv3 license, as written in the included LICENSE.txt file.
4 	Authors: Sönke Ludwig
5 */
6 module dubregistry.repositories.bitbucket;
7 
8 import dubregistry.cache;
9 import dubregistry.dbcontroller : DbRepository;
10 import dubregistry.repositories.repository;
11 import std..string : format, startsWith;
12 import std.typecons;
13 import vibe.core.log;
14 import vibe.core.stream;
15 import vibe.data.json;
16 import vibe.inet.url;
17 
18 
19 class BitbucketRepository : Repository {
20 @safe:
21 
22 	private {
23 		string m_owner;
24 		string m_project;
25 	}
26 
27 	static void register()
28 	{
29 		Repository factory(DbRepository info) @safe {
30 			return new BitbucketRepository(info.owner, info.project);
31 		}
32 		addRepositoryFactory("bitbucket", &factory);
33 	}
34 
35 	this(string owner, string project)
36 	{
37 		m_owner = owner;
38 		m_project = project;
39 	}
40 
41 	RefInfo[] getTags()
42 	{
43 		Json tags;
44 		try tags = readJson("https://api.bitbucket.org/1.0/repositories/"~m_owner~"/"~m_project~"/tags");
45 		catch( Exception e ) { throw new Exception("Failed to get tags: "~e.msg); }
46 		RefInfo[] ret;
47 		foreach (string tagname, tag; tags.byKeyValue) {
48 			try {
49 				auto commit_hash = tag["raw_node"].get!string();
50 				auto commit_date = bbToIsoDate(tag["utctimestamp"].get!string());
51 				ret ~= RefInfo(tagname, commit_hash, commit_date);
52 				logDebug("Found tag for %s/%s: %s", m_owner, m_project, tagname);
53 			} catch( Exception e ){
54 				throw new Exception("Failed to process tag "~tag["name"].get!string~": "~e.msg);
55 			}
56 		}
57 		return ret;
58 	}
59 
60 	RefInfo[] getBranches()
61 	{
62 		Json branches = readJson("https://api.bitbucket.org/1.0/repositories/"~m_owner~"/"~m_project~"/branches");
63 		RefInfo[] ret;
64 		foreach (string branchname, branch; branches.byKeyValue) {
65 			auto commit_hash = branch["raw_node"].get!string();
66 			auto commit_date = bbToIsoDate(branch["utctimestamp"].get!string());
67 			ret ~= RefInfo(branchname, commit_hash, commit_date);
68 			logDebug("Found branch for %s/%s: %s", m_owner, m_project, branchname);
69 		}
70 		return ret;
71 	}
72 
73 	RepositoryInfo getInfo()
74 	{
75 		auto nfo = readJson("https://api.bitbucket.org/1.0/repositories/"~m_owner~"/"~m_project);
76 		RepositoryInfo ret;
77 		ret.isFork = nfo["is_fork"].opt!bool;
78 		ret.stats.watchers = nfo["followers_count"].opt!uint;
79 		ret.stats.forks = nfo["forks_count"].opt!uint;
80 		return ret;
81 	}
82 
83 	RepositoryFile[] listFiles(string commit_sha, InetPath path)
84 	{
85 		assert(path.absolute, "Passed relative path to listFiles.");
86 		auto url = "https://bitbucket.org/api/2.0/repositories/"~m_owner~"/"~m_project~"/src/"~commit_sha~path.toString()~"?pagelen=100";
87 		auto ls = readJson(url)["values"].get!(Json[]);
88 		RepositoryFile[] ret;
89 		ret.reserve(ls.length);
90 		foreach (entry; ls) {
91 			string type = entry["type"].get!string;
92 			RepositoryFile file;
93 			if (type == "commit_directory") {
94 				file.type = RepositoryFile.Type.directory;
95 			}
96 			else if (type == "commit_file") {
97 				file.type = RepositoryFile.Type.file;
98 				file.size = entry["size"].get!size_t;
99 			}
100 			else continue;
101 			file.commitSha = entry["commit"]["hash"].get!string;
102 			file.path = InetPath("/" ~ entry["path"].get!string);
103 			ret ~= file;
104 		}
105 		return ret;
106 	}
107 
108 	void readFile(string commit_sha, InetPath path, scope void delegate(scope InputStream) @safe reader)
109 	{
110 		assert(path.absolute, "Passed relative path to readFile.");
111 		auto url = "https://bitbucket.org/api/1.0/repositories/"~m_owner~"/"~m_project~"/raw/"~commit_sha~path.toString();
112 		downloadCached(url, (scope input) @safe {
113 			reader(input);
114 		}, true);
115 	}
116 
117 	string getDownloadUrl(string ver)
118 	{
119 		import std.uri : encodeComponent;
120 		if( ver.startsWith("~") ) ver = ver[1 .. $];
121 		else ver = ver;
122 		auto venc = () @trusted { return encodeComponent(ver); } ();
123 		return "https://bitbucket.org/"~m_owner~"/"~m_project~"/get/"~venc~".zip";
124 	}
125 
126 	void download(string ver, scope void delegate(scope InputStream) @safe del)
127 	{
128 		downloadCached(getDownloadUrl(ver), del);
129 	}
130 }
131 
132 private auto bbToIsoDate(string bbdate)
133 @safe {
134 	import std.array, std.datetime : SysTime;
135 	auto ttz = bbdate.split("+");
136 	if( ttz.length < 2 ) ttz ~= "00:00";
137 	auto parts = ttz[0].split("-");
138 	parts = parts[0 .. $-1] ~ parts[$-1].split(" ");
139 	parts = parts[0 .. $-1] ~ parts[$-1].split(":");
140 
141 	return SysTime.fromISOString(format("%s%s%sT%s%s%s+%s", parts[0], parts[1], parts[2], parts[3], parts[4], parts[5], ttz[1]));
142 }