1 /*
2  *             Copyright Andrej Mitrovic 2013.
3  *  Distributed under the Boost Software License, Version 1.0.
4  *     (See accompanying file LICENSE_1_0.txt or copy at
5  *           http://www.boost.org/LICENSE_1_0.txt)
6  */
7 module git.oid;
8 
9 import core.exception;
10 
11 import std.algorithm;
12 import std.exception;
13 import std.conv;
14 import std.range;
15 import std.stdio;
16 import std.string;
17 import std.typecons;
18 
19 import deimos.git2.common;
20 import deimos.git2.oid;
21 import deimos.git2.types;
22 
23 import git.exception;
24 import git.util;
25 
26 /**
27     The unique ID of any Git object, whether it's a commit,
28     tree, blob, tag, etc.
29 */
30 struct GitOid
31 {
32     /** Size (in bytes) of a raw/binary oid */
33     enum BinarySize = GIT_OID_RAWSZ;
34 
35     /** Minimum length (in number of hex characters,
36      * i.e. packets of 4 bits) of an oid prefix */
37     enum MinHexSize = GIT_OID_MINPREFIXLEN;
38 
39     /** Size (in bytes) of a hex formatted oid */
40     enum MaxHexSize = GIT_OID_HEXSZ;
41 
42     /**
43         Parse a full or partial hex-formatted object ID and
44         create a GitOid object.
45 
46         $(D hex) must be at least the size of $(D MinHexSize),
47         but not larger than $(D MaxHexSize).
48     */
49     this(in char[] hex)
50     {
51         assert(hex.length >= MinHexSize && hex.length <= MaxHexSize);
52         require(git_oid_fromstrn(&_oid, hex.ptr, hex.length) == 0);
53     }
54 
55     ///
56     unittest
57     {
58         // note: don't use an enum due to http://d.puremagic.com/issues/show_bug.cgi?id=10516
59         const srcHex = "49322bb17d3acc9146f98c97d078513228bbf3c0";
60         const oid = GitOid(srcHex);
61         const tgtHex = oid.toHex();
62 
63         assert(tgtHex == srcHex);
64     }
65 
66     ///
67     unittest
68     {
69         // can convert from a partial string
70         const srcHex = "4932";
71         const oid = GitOid(srcHex);
72         const tgtHex = oid.toHex();
73 
74         assert(tgtHex[0 .. 4] == srcHex);
75         assert(tgtHex[4 .. $].count('0') == tgtHex.length - 4);
76     }
77 
78     ///
79     unittest
80     {
81         /// cannot convert from a partial string smaller than MinHexSize
82         const smallHex = "493";
83         assertThrown!AssertError(GitOid(smallHex));
84 
85         /// cannot convert from a string bigger than MinHexSize
86         const bigHex = std.array.replicate("1", MaxHexSize + 1);
87         assertThrown!AssertError(GitOid(bigHex));
88     }
89 
90     // internal
91     package this(inout(git_oid) oid)
92     {
93         _oid = oid;
94     }
95 
96     ///
97     unittest
98     {
99         git_oid oid1;
100         const(git_oid) oid2;
101 
102         GitOid(oid1);
103         GitOid(oid2);
104     }
105 
106     @property ref inout(ubyte[20]) bytes() inout { return _oid.id; }
107 
108     /**
109         Convert this GitOid into a hex string.
110 
111         $(B Note): If this oid has been constructed with a partial
112         hex string, $(D toHex) will still return a hex string of size
113         $(D MaxHexSize) but with padded zeros.
114     */
115     string toHex() const
116     {
117         auto buffer = new char[](MaxHexSize);
118         git_oid_nfmt(buffer.ptr, MaxHexSize, &_oid);
119         return assumeUnique(buffer);
120     }
121 
122     ///
123     unittest
124     {
125         // convert hex to oid and back to hex
126         const hex = "49322bb17d3acc9146f98c97d078513228bbf3c0";
127         const oid = GitOid(hex);
128         assert(oid.toHex == hex);
129     }
130 
131     ///
132     unittest
133     {
134         // convert partial hex to oid and back to hex
135         const hex = "4932";
136         const oid = GitOid(hex);
137         assert(oid.toHex == "4932000000000000000000000000000000000000");
138     }
139 
140     /// Return the string representation of this Oid, in hex form.
141     string toString() const
142     {
143         return format("GitOid(%s)", toHex.toUpper);
144     }
145 
146     ///
147     unittest
148     {
149         // convert hex to oid and back to hex
150         const hex = "49322bb17d3acc9146f98c97d078513228bbf3c0";
151         const oid = GitOid(hex);
152         assert(to!string(oid) == format("GitOid(%s)", hex.toUpper));
153     }
154 
155     bool opCast(T)() const if (is(T == bool)) { return git_oid_iszero(&_oid) == 0; }
156 
157 package:
158     // internal use only
159     ref inout(git_oid) _get_oid() inout { return _oid; }
160 
161 private:
162     git_oid _oid;
163 }
164 
165 /**
166     The OID shortener is used to process a list of OIDs
167     in text form and return the shortest length that would
168     uniquely identify all of them.
169 
170     $(B Note): This is a refcounted type and can be
171     safely copied by value. The underyling C handle will be
172     released once the reference count reaches zero.
173 */
174 struct GitOidShorten
175 {
176     /// Default-construction is disabled
177     @disable this();
178 
179     ///
180     unittest
181     {
182         // default construction is disabled
183         static assert(!__traits(compiles, GitOidShorten()));
184     }
185 
186     /**
187         Create a new OID shortener. $(D length) is the minimum length
188         which will be used to shorted the OIDs, even if a shorter
189         length can uniquely identify all OIDs.
190     */
191     this(size_t length)
192     {
193         enforceEx!GitException(length <= GitOid.MaxHexSize,
194                                format("Error: Minimum hex length cannot be larger than '%s' (GitOid.MaxHexSize), it is '%s'", GitOid.MaxHexSize, length));
195 
196         _data = Data(length);
197         _minLength = length;
198     }
199 
200     ///
201     unittest
202     {
203         auto oidShort = GitOidShorten(10);
204         assert(oidShort.minLength == 10);
205 
206         // cannot be larger than MaxHexSize
207         assertThrown!GitException(GitOidShorten(GitOid.MaxHexSize + 1));
208     }
209 
210     /**
211         Add a new hex OID to the set of shortened OIDs and store
212         the minimal length to uniquely identify all the OIDs in
213         the set. This length can then be retrieved by calling $(D minLength).
214 
215         $(B Note:) The hex OID must be a 40-char hexadecimal string. Calling
216         $(D add) with a shorter OID will throw a GitOid exception.
217 
218         For performance reasons, there is a hard-limit of how many
219         OIDs can be added to a single set - around ~22000, assuming
220         a mostly randomized distribution.
221 
222         Attempting to go over this limit will throw a $(D GitException).
223     */
224     void add(in char[] hex)
225     {
226         enforceEx!GitException(hex.length == GitOid.MaxHexSize,
227                                format("Error: Hex string size must be equal to '%s' (GitOid.MaxHexSize), not '%s'", GitOid.MaxHexSize, hex.length));
228 
229         auto result = git_oid_shorten_add(_data._payload, hex.ptr);
230         require(result >= 0);
231         _minLength = result;
232     }
233 
234     ///
235     unittest
236     {
237         auto sh = GitOidShorten(5);
238         assert(sh.minLength == 5);
239 
240         sh.add("1234000000000000000000000000000000000000");
241         assert(sh.minLength == 5);
242 
243         sh.add("1234500000000000000000000000000000000000");
244         assert(sh.minLength == 5);
245 
246         // introduce conflicting oid which requires a
247         // larger length for unique identification in the set
248         sh.add("1234560000000000000000000000000000000000");
249         assert(sh.minLength == 6);
250     }
251 
252     ///
253     unittest
254     {
255         // adding a shortened hex is disallowed
256         auto sh = GitOidShorten(5);
257         assertThrown!GitException(sh.add("1234"));
258     }
259 
260     /** Return the current minimum length used to uniquely identify all the stored OIDs. */
261     @property size_t minLength() { return _minLength; }
262 
263 private:
264 
265     /** Payload for the $(D git_oid_shorten) object which should be refcounted. */
266     struct Payload
267     {
268         this(size_t length)
269         {
270             _payload = enforce(git_oid_shorten_new(length), "Error: Out of memory.");
271         }
272 
273         ~this()
274         {
275             //~ writefln("- %s", __FUNCTION__);
276 
277             if (_payload !is null)
278             {
279                 git_oid_shorten_free(_payload);
280                 _payload = null;
281             }
282         }
283 
284         /// Should never perform copy
285         @disable this(this);
286 
287         /// Should never perform assign
288         @disable void opAssign(typeof(this));
289 
290         git_oid_shorten* _payload;
291     }
292 
293     // refcounted git_oid_shorten
294     alias RefCounted!(Payload, RefCountedAutoInitialize.no) Data;
295     Data _data;
296 
297     // current minimum length
298     size_t _minLength;
299 }