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 }