1 /**
2 	Support for GitLab repositories.
3 
4 	Copyright: © 2015-2016 rejectedsoftware e.K.
5 	License: Subject to the terms of the GNU GPLv3 license, as written in the included LICENSE.txt file.
6 	Authors: Sönke Ludwig
7 */
8 module dubregistry.repositories.gitlab;
9 
10 import dubregistry.cache;
11 import dubregistry.dbcontroller : DbRepository;
12 import dubregistry.repositories.repository;
13 import std.string : startsWith;
14 import std.typecons;
15 import vibe.core.log;
16 import vibe.core.stream;
17 import vibe.data.json;
18 import vibe.inet.url;
19 import vibe.textfilter.urlencode;
20 
21 
22 class GitLabRepository : Repository {
23 @safe:
24 
25 	private {
26 		string m_owner;
27 		string m_projectPath;
28 		URL m_baseURL;
29 		string m_authToken;
30 	}
31 
32 	static void register(string auth_token, string url)
33 	{
34 		Repository factory(DbRepository info) @safe {
35 			return new GitLabRepository(info.owner, info.project, auth_token, url.length ? URL(url) : URL("https://gitlab.com/"));
36 		}
37 		addRepositoryFactory("gitlab", &factory);
38 	}
39 
40 	this(string owner, string projectPath, string auth_token, URL base_url)
41 	{
42 		m_owner = owner;
43 		m_projectPath = projectPath;
44 		m_authToken = auth_token;
45 		m_baseURL = base_url;
46 	}
47 
48 	RefInfo[] getTags()
49 	{
50 		import std.datetime.systime : SysTime;
51 
52 		Json tags;
53 		try tags = readJson(getAPIURLPrefix()~"repository/tags?private_token="~m_authToken);
54 		catch( Exception e ) { throw new Exception("Failed to get tags: "~e.msg); }
55 		RefInfo[] ret;
56 		foreach_reverse (tag; tags) {
57 			try {
58 				auto tagname = tag["name"].get!string;
59 				auto commit = tag["commit"]["id"].get!string;
60 				auto date = SysTime.fromISOExtString(tag["commit"]["committed_date"].get!string);
61 				ret ~= RefInfo(tagname, commit, date);
62 				logDebug("Found tag for %s/%s: %s", m_owner, m_projectPath, tagname);
63 			} catch( Exception e ){
64 				throw new Exception("Failed to process tag "~tag["name"].get!string~": "~e.msg);
65 			}
66 		}
67 		return ret;
68 	}
69 
70 	RefInfo[] getBranches()
71 	{
72 		import std.datetime.systime : SysTime;
73 
74 		Json branches = readJson(getAPIURLPrefix()~"repository/branches?private_token="~m_authToken);
75 		RefInfo[] ret;
76 		foreach_reverse( branch; branches ){
77 			auto branchname = branch["name"].get!string;
78 			auto commit = branch["commit"]["id"].get!string;
79 			auto date = SysTime.fromISOExtString(branch["commit"]["committed_date"].get!string);
80 			ret ~= RefInfo(branchname, commit, date);
81 			logDebug("Found branch for %s/%s: %s", m_owner, m_projectPath, branchname);
82 		}
83 		return ret;
84 	}
85 
86 	RepositoryInfo getInfo()
87 	{
88 		RepositoryInfo ret;
89 		auto nfo = readJson(getAPIURLPrefix()~"?private_token="~m_authToken);
90 		ret.isFork = false; // not reported by API
91 		ret.stats.stars = nfo["star_count"].opt!uint; // might mean watchers for Gitlab
92 		ret.stats.forks = nfo["forks_count"].opt!uint;
93 		ret.stats.issues = nfo["open_issues_count"].opt!uint;
94 		return ret;
95 	}
96 
97 	RepositoryFile[] listFiles(string commit_sha, InetPath path)
98 	{
99 		import std.uri : encodeComponent;
100 		assert(path.absolute, "Passed relative path to listFiles.");
101 		auto penc = () @trusted { return encodeComponent(path.toString()[1..$]); } ();
102 		auto url = getAPIURLPrefix()~"repository/tree?path="~penc~"&ref="~commit_sha;
103 		auto ls = readJson(url).get!(Json[]);
104 		RepositoryFile[] ret;
105 		ret.reserve(ls.length);
106 		foreach (entry; ls) {
107 			string type = entry["type"].get!string;
108 			RepositoryFile file;
109 			if (type == "tree") {
110 				file.type = RepositoryFile.Type.directory;
111 			}
112 			else if (type == "blob") {
113 				file.type = RepositoryFile.Type.file;
114 			}
115 			else continue;
116 			file.commitSha = commit_sha;
117 			file.path = InetPath("/" ~ entry["path"].get!string);
118 			ret ~= file;
119 		}
120 		return ret;
121 	}
122 
123 	void readFile(string commit_sha, InetPath path, scope void delegate(scope InputStream) @safe reader)
124 	{
125 		assert(path.absolute, "Passed relative path to readFile.");
126 		auto penc = path.toString()[1..$].urlEncode;
127 		auto url = getAPIURLPrefix() ~ "repository/files/" ~ penc ~ "/raw?ref=" ~ commit_sha ~ "&private_token="~ m_authToken;
128 		downloadCached(url, (scope input) {
129 			reader(input);
130 		}, true);
131 	}
132 
133 	string getDownloadUrl(string ver)
134 	{
135 		if (m_authToken.length > 0) return null; // public download URL doesn't work
136 		return getRawDownloadURL(ver);
137 	}
138 
139 	void download(string ver, scope void delegate(scope InputStream) @safe del)
140 	{
141 		auto url = getRawDownloadURL(ver);
142 		url ~= "&private_token="~m_authToken;
143 		downloadCached(url, del);
144 	}
145 
146 	private string getRawDownloadURL(string ver)
147 	{
148 		import std.uri : encodeComponent;
149 		if (ver.startsWith("~")) ver = ver[1 .. $];
150 		else ver = ver;
151 		auto venc = () @trusted { return encodeComponent(ver); } ();
152 		// The "sha" parameter in GitLab's API v4 accepts the tag, branch or the  the commit sha (see https://docs.gitlab.com/ee/api/repositories.html#get-file-archive)
153 		return getAPIURLPrefix() ~ "repository/archive.zip?sha="~venc;
154 	}
155 
156 	private string getAPIURLPrefix()
157 	{
158 		return m_baseURL.toString() ~ "api/v4/projects/" ~ (m_owner ~ "/" ~ m_projectPath).urlEncode ~ "/";
159 	}
160 }