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 git2.common;
20 import git2.oid;
21 import 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     /**
107         Convert this GitOid into a hex string.
108 
109         $(B Note): If this oid has been constructed with a partial
110         hex string, $(D toHex) will still return a hex string of size
111         $(D MaxHexSize) but with padded zeros.
112     */
113     string toHex() const
114     {
115         auto buffer = new char[](MaxHexSize);
116         git_oid_nfmt(buffer.ptr, MaxHexSize, &_oid);
117         return assumeUnique(buffer);
118     }
119 
120     ///
121     unittest
122     {
123         // convert hex to oid and back to hex
124         const hex = "49322bb17d3acc9146f98c97d078513228bbf3c0";
125         const oid = GitOid(hex);
126         assert(oid.toHex == hex);
127     }
128 
129     ///
130     unittest
131     {
132         // convert partial hex to oid and back to hex
133         const hex = "4932";
134         const oid = GitOid(hex);
135         assert(oid.toHex == "4932000000000000000000000000000000000000");
136     }
137 
138     /// Return the string representation of this Oid, in hex form.
139     string toString() const
140     {
141         return format("GitOid(%s)", toHex.toUpper);
142     }
143 
144     ///
145     unittest
146     {
147         // convert hex to oid and back to hex
148         const hex = "49322bb17d3acc9146f98c97d078513228bbf3c0";
149         const oid = GitOid(hex);
150         assert(to!string(oid) == format("GitOid(%s)", hex.toUpper));
151     }
152 
153 package:
154     // internal use only
155     git_oid _get_oid() { return _oid; }
156 
157 private:
158     git_oid _oid;
159 }
160 
161 /**
162     The OID shortener is used to process a list of OIDs
163     in text form and return the shortest length that would
164     uniquely identify all of them.
165 
166     $(B Note): This is a refcounted type and can be
167     safely copied by value. The underyling C handle will be
168     released once the reference count reaches zero.
169 */
170 struct GitOidShorten
171 {
172     /// Default-construction is disabled
173     @disable this();
174 
175     ///
176     unittest
177     {
178         // default construction is disabled
179         static assert(!__traits(compiles, GitOidShorten()));
180     }
181 
182     /**
183         Create a new OID shortener. $(D length) is the minimum length
184         which will be used to shorted the OIDs, even if a shorter
185         length can uniquely identify all OIDs.
186     */
187     this(size_t length)
188     {
189         enforceEx!GitException(length <= GitOid.MaxHexSize,
190                                format("Error: Minimum hex length cannot be larger than '%s' (GitOid.MaxHexSize), it is '%s'", GitOid.MaxHexSize, length));
191 
192         _data = Data(length);
193         _minLength = length;
194     }
195 
196     ///
197     unittest
198     {
199         auto oidShort = GitOidShorten(10);
200         assert(oidShort.minLength == 10);
201 
202         // cannot be larger than MaxHexSize
203         assertThrown!GitException(GitOidShorten(GitOid.MaxHexSize + 1));
204     }
205 
206     /**
207         Add a new hex OID to the set of shortened OIDs and store
208         the minimal length to uniquely identify all the OIDs in
209         the set. This length can then be retrieved by calling $(D minLength).
210 
211         $(B Note:) The hex OID must be a 40-char hexadecimal string. Calling
212         $(D add) with a shorter OID will throw a GitOid exception.
213 
214         For performance reasons, there is a hard-limit of how many
215         OIDs can be added to a single set - around ~22000, assuming
216         a mostly randomized distribution.
217 
218         Attempting to go over this limit will throw a $(D GitException).
219     */
220     void add(in char[] hex)
221     {
222         enforceEx!GitException(hex.length == GitOid.MaxHexSize,
223                                format("Error: Hex string size must be equal to '%s' (GitOid.MaxHexSize), not '%s'", GitOid.MaxHexSize, hex.length));
224 
225         auto result = git_oid_shorten_add(_data._payload, hex.ptr);
226         require(result >= 0);
227         _minLength = result;
228     }
229 
230     ///
231     unittest
232     {
233         auto sh = GitOidShorten(5);
234         assert(sh.minLength == 5);
235 
236         sh.add("1234000000000000000000000000000000000000");
237         assert(sh.minLength == 5);
238 
239         sh.add("1234500000000000000000000000000000000000");
240         assert(sh.minLength == 5);
241 
242         // introduce conflicting oid which requires a
243         // larger length for unique identification in the set
244         sh.add("1234560000000000000000000000000000000000");
245         assert(sh.minLength == 6);
246     }
247 
248     ///
249     unittest
250     {
251         // adding a shortened hex is disallowed
252         auto sh = GitOidShorten(5);
253         assertThrown!GitException(sh.add("1234"));
254     }
255 
256     /** Return the current minimum length used to uniquely identify all the stored OIDs. */
257     @property size_t minLength() { return _minLength; }
258 
259 private:
260 
261     /** Payload for the $(D git_oid_shorten) object which should be refcounted. */
262     struct Payload
263     {
264         this(size_t length)
265         {
266             _payload = enforce(git_oid_shorten_new(length), "Error: Out of memory.");
267         }
268 
269         ~this()
270         {
271             //~ writefln("- %s", __FUNCTION__);
272 
273             if (_payload !is null)
274             {
275                 git_oid_shorten_free(_payload);
276                 _payload = null;
277             }
278         }
279 
280         /// Should never perform copy
281         @disable this(this);
282 
283         /// Should never perform assign
284         @disable void opAssign(typeof(this));
285 
286         git_oid_shorten* _payload;
287     }
288 
289     // refcounted git_oid_shorten
290     alias RefCounted!(Payload, RefCountedAutoInitialize.no) Data;
291     Data _data;
292 
293     // current minimum length
294     size_t _minLength;
295 }